diff --git a/.github/workflows/auto-reply.yml b/.github/workflows/auto-reply.yml new file mode 100644 index 0000000..1dc440a --- /dev/null +++ b/.github/workflows/auto-reply.yml @@ -0,0 +1,23 @@ +name: Auto Reply to New PRs + +on: + pull_request: + types: [opened] + +jobs: + welcome: + runs-on: ubuntu-latest + permissions: + pull-requests: write # Grant write permission for creating comments + steps: + - name: Post Welcome Message + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Thank you for opening this Pull Request! This repository is for demo purposes only. It's not maintained and there is no CI or merge rules. If you have permissions, you're free to merge the PR without review. If you'd like a review, please explicitly request it." + }) \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index e00e633..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Main Branch CI - -# Declare default permissions as read only. -permissions: read-all - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - schedule: - - cron: "0 0 * * *" # Every day at midnight - -defaults: - run: - shell: bash - -jobs: - flutter-tests: - name: Test Flutter ${{ matrix.flutter_version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - if: github.repository == 'flutter/demos' - strategy: - fail-fast: false - matrix: - flutter_version: [stable, beta, master] - os: [ubuntu-latest, macos-latest, windows-latest] - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b - with: - distribution: 'zulu' - java-version: '17' - - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 - with: - channel: ${{ matrix.flutter_version }} - - run: ./tool/flutter_ci_script_${{ matrix.flutter_version }}.sh diff --git a/.gitignore b/.gitignore index 1af98a5..7bfe92a 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,11 @@ yarn.lock !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +/graalvm_test/build +/graalvm_test_native_interop/build +crossword_companion/firebase.json +crossword_companion/lib/firebase_options.dart +native_interop_demos/ios_calendar_demo/lib/eventkit_bindings.dart +native_interop_demos/ios_calendar_demo/ios/Runner/eventkit_wrapper.m +native_interop_demos/ios_calendar_demo/ios/Runner/eventkit_wrapper.swift +/native_interop_demos/ios_calendar_demo/temp diff --git a/compass_25/.gitignore b/compass_25/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/compass_25/.gitignore @@ -0,0 +1,45 @@ +# 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 +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# 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 diff --git a/compass_25/.metadata b/compass_25/.metadata new file mode 100644 index 0000000..2b7c071 --- /dev/null +++ b/compass_25/.metadata @@ -0,0 +1,30 @@ +# 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: "fce807530a201ddbd18d66be2b3295bbe209f389" + channel: "beta" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: fce807530a201ddbd18d66be2b3295bbe209f389 + base_revision: fce807530a201ddbd18d66be2b3295bbe209f389 + - platform: ios + create_revision: fce807530a201ddbd18d66be2b3295bbe209f389 + base_revision: fce807530a201ddbd18d66be2b3295bbe209f389 + + # 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' diff --git a/compass_25/README.md b/compass_25/README.md new file mode 100644 index 0000000..43e182e --- /dev/null +++ b/compass_25/README.md @@ -0,0 +1,6 @@ +# compass + +A Flutter sample demonstrating adaptive Material and Cupertino widgets, +Firebase integration, and a custom brand-rich design. + +![Screenshot](docs/walkthrough.gif) diff --git a/compass_25/analysis_options.yaml b/compass_25/analysis_options.yaml new file mode 100644 index 0000000..8feb046 --- /dev/null +++ b/compass_25/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + errors: + library_private_types_in_public_api: ignore diff --git a/compass_25/android/.gitignore b/compass_25/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/compass_25/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/compass_25/android/app/build.gradle.kts b/compass_25/android/app/build.gradle.kts new file mode 100644 index 0000000..6c1ca2f --- /dev/null +++ b/compass_25/android/app/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.cupertino_compass" + compileSdk = flutter.compileSdkVersion + ndkVersion = "27.0.12077973" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.cupertino_compass" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/compass_25/android/app/google-services.json b/compass_25/android/app/google-services.json new file mode 100644 index 0000000..c3b9d24 --- /dev/null +++ b/compass_25/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "1025902230344", + "project_id": "compass-ios-455022", + "storage_bucket": "compass-ios-455022.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1025902230344:android:5623a28d48302226b6117d", + "android_client_info": { + "package_name": "com.example.cupertino_compass" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyD5s-frlzmlN8xRuClgdZPJ00HKZVgBd7A" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/compass_25/android/app/src/debug/AndroidManifest.xml b/compass_25/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/compass_25/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/compass_25/android/app/src/main/AndroidManifest.xml b/compass_25/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..91c9071 --- /dev/null +++ b/compass_25/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/compass_25/android/app/src/main/kotlin/com/example/cupertino_compass/MainActivity.kt b/compass_25/android/app/src/main/kotlin/com/example/cupertino_compass/MainActivity.kt new file mode 100644 index 0000000..be3056c --- /dev/null +++ b/compass_25/android/app/src/main/kotlin/com/example/cupertino_compass/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.cupertino_compass + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/compass_25/android/app/src/main/res/drawable-v21/launch_background.xml b/compass_25/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/compass_25/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/compass_25/android/app/src/main/res/drawable/launch_background.xml b/compass_25/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/compass_25/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/compass_25/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/compass_25/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/compass_25/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/compass_25/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/compass_25/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/compass_25/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/compass_25/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/compass_25/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/compass_25/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/compass_25/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/compass_25/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/compass_25/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/compass_25/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/compass_25/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/compass_25/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/compass_25/android/app/src/main/res/values-night/styles.xml b/compass_25/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/compass_25/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/compass_25/android/app/src/main/res/values/styles.xml b/compass_25/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/compass_25/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/compass_25/android/app/src/profile/AndroidManifest.xml b/compass_25/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/compass_25/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/compass_25/android/build.gradle.kts b/compass_25/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/compass_25/android/build.gradle.kts @@ -0,0 +1,21 @@ +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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/compass_25/android/gradle.properties b/compass_25/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/compass_25/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/compass_25/android/gradle/wrapper/gradle-wrapper.properties b/compass_25/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afa1e8e --- /dev/null +++ b/compass_25/android/gradle/wrapper/gradle-wrapper.properties @@ -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.10.2-all.zip diff --git a/compass_25/android/settings.gradle.kts b/compass_25/android/settings.gradle.kts new file mode 100644 index 0000000..9e2d35c --- /dev/null +++ b/compass_25/android/settings.gradle.kts @@ -0,0 +1,28 @@ +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.7.0" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/compass_25/assets/activities.json b/compass_25/assets/activities.json new file mode 100644 index 0000000..136e048 --- /dev/null +++ b/compass_25/assets/activities.json @@ -0,0 +1,32678 @@ +[ + { + "name": "Glacier Trekking and Ice Climbing", + "description": "Embark on a thrilling adventure exploring the awe-inspiring glaciers of Alaska. Hike across the icy terrain, marvel at the deep blue crevasses, and even try your hand at ice climbing for an unforgettable experience.", + "locationName": "Matanuska Glacier or Mendenhall Glacier", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "alaska", + "ref": "glacier-trekking-and-ice-climbing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_glacier-trekking-and-ice-climbing.jpg" + }, + { + "name": "Wildlife Viewing Cruise", + "description": "Set sail on a scenic cruise through Alaska's pristine waters and witness the abundance of wildlife in their natural habitat. Keep an eye out for majestic whales, playful sea otters, and soaring bald eagles.", + "locationName": "Kenai Fjords National Park or Glacier Bay National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "alaska", + "ref": "wildlife-viewing-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_wildlife-viewing-cruise.jpg" + }, + { + "name": "Dog Sledding Experience", + "description": "Immerse yourself in Alaska's rich sled dog culture with an exhilarating dog sledding tour. Feel the rush as a team of huskies pulls you across the snowy landscape, offering a unique and unforgettable way to explore the Alaskan wilderness.", + "locationName": "Various locations throughout Alaska", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "alaska", + "ref": "dog-sledding-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_dog-sledding-experience.jpg" + }, + { + "name": "Hiking in Denali National Park", + "description": "Lace up your hiking boots and embark on a scenic trek through Denali National Park, home to North America's highest peak. Explore diverse trails, surrounded by breathtaking mountain vistas, glaciers, and abundant wildlife.", + "locationName": "Denali National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "alaska", + "ref": "hiking-in-denali-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_hiking-in-denali-national-park.jpg" + }, + { + "name": "Visit the Alaska Native Heritage Center", + "description": "Immerse yourself in the rich culture and traditions of Alaska's indigenous people at the Alaska Native Heritage Center. Explore exhibits, watch traditional dances, and learn about the history and way of life of Alaska's native communities.", + "locationName": "Anchorage", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "alaska", + "ref": "visit-the-alaska-native-heritage-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_visit-the-alaska-native-heritage-center.jpg" + }, + { + "name": "Northern Lights Viewing", + "description": "Witness the mesmerizing spectacle of the Aurora Borealis dancing across the night sky. Venture outside Fairbanks or Anchorage during the winter months for the best views, and be captivated by the vibrant hues of green, purple, and pink.", + "locationName": "Fairbanks or Anchorage", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "alaska", + "ref": "northern-lights-viewing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_northern-lights-viewing.jpg" + }, + { + "name": "Kenai Fjords National Park Cruise", + "description": "Embark on a scenic cruise through the stunning Kenai Fjords National Park. Marvel at towering glaciers, rugged coastlines, and abundant marine life, including whales, seals, and puffins.", + "locationName": "Kenai Fjords National Park", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "alaska", + "ref": "kenai-fjords-national-park-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_kenai-fjords-national-park-cruise.jpg" + }, + { + "name": "Flightseeing over Denali", + "description": "Soar above North America's highest peak, Denali, on a breathtaking flightseeing tour. Enjoy panoramic views of the snow-capped mountains, glaciers, and vast wilderness below, offering a unique perspective of the Alaskan landscape.", + "locationName": "Denali National Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "alaska", + "ref": "flightseeing-over-denali", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_flightseeing-over-denali.jpg" + }, + { + "name": "Gold Panning in Fairbanks", + "description": "Step back in time and experience the thrill of the Alaskan Gold Rush. Visit a gold panning site in Fairbanks and try your luck at finding precious nuggets, learning about the region's rich mining history.", + "locationName": "Fairbanks", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "alaska", + "ref": "gold-panning-in-fairbanks", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_gold-panning-in-fairbanks.jpg" + }, + { + "name": "Fishing for Salmon", + "description": "Cast your line into Alaska's pristine rivers and lakes for an unforgettable fishing experience. Whether you're a seasoned angler or a beginner, enjoy the thrill of catching wild salmon amidst the stunning natural beauty.", + "locationName": "Kenai River, Russian River, or Kasilof River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "alaska", + "ref": "fishing-for-salmon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_fishing-for-salmon.jpg" + }, + { + "name": "Kayaking in the Kenai Fjords National Park", + "description": "Paddle through the pristine waters of Kenai Fjords National Park, surrounded by towering glaciers, dramatic cliffs, and abundant marine life. Witness harbor seals basking on icebergs, sea otters playing in kelp forests, and perhaps even catch a glimpse of whales breaching the surface. This unforgettable kayaking experience offers a unique perspective on Alaska's stunning coastal landscapes.", + "locationName": "Kenai Fjords National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "alaska", + "ref": "kayaking-in-the-kenai-fjords-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_kayaking-in-the-kenai-fjords-national-park.jpg" + }, + { + "name": "Visit the Alaska SeaLife Center", + "description": "Discover the wonders of Alaska's marine ecosystem at the Alaska SeaLife Center, a renowned research and rehabilitation facility. Get up close to fascinating creatures like puffins, octopus, sea lions, and harbor seals. Learn about ongoing conservation efforts and gain a deeper appreciation for the delicate balance of life in Alaska's oceans.", + "locationName": "Seward", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "alaska", + "ref": "visit-the-alaska-sealife-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_visit-the-alaska-sealife-center.jpg" + }, + { + "name": "Ride the White Pass & Yukon Route Railway", + "description": "Embark on a nostalgic journey aboard the historic White Pass & Yukon Route Railway, a narrow-gauge railroad that winds through breathtaking mountain scenery. Travel along the same tracks used during the Klondike Gold Rush and marvel at the engineering feats that made this railway possible. Enjoy panoramic views of glaciers, waterfalls, and historic mining towns.", + "locationName": "Skagway", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "alaska", + "ref": "ride-the-white-pass-yukon-route-railway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_ride-the-white-pass-yukon-route-railway.jpg" + }, + { + "name": "Explore the Mendenhall Glacier", + "description": "Witness the awe-inspiring beauty of the Mendenhall Glacier, a massive ice formation just outside of Juneau. Hike to the glacier's edge, explore ice caves, or take a thrilling helicopter tour for a bird's-eye view. Learn about the glacier's formation, its impact on the surrounding ecosystem, and the effects of climate change.", + "locationName": "Juneau", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "alaska", + "ref": "explore-the-mendenhall-glacier", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_explore-the-mendenhall-glacier.jpg" + }, + { + "name": "Soak in Chena Hot Springs", + "description": "Relax and rejuvenate in the natural mineral waters of Chena Hot Springs, a geothermal oasis nestled in the Alaskan wilderness. Immerse yourself in the therapeutic pools, surrounded by snow-capped mountains and boreal forests. Enhance your experience with a spa treatment or explore the Aurora Ice Museum, a unique attraction carved entirely from ice.", + "locationName": "Fairbanks", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "alaska", + "ref": "soak-in-chena-hot-springs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_soak-in-chena-hot-springs.jpg" + }, + { + "name": "Bear Viewing at Katmai National Park", + "description": "Embark on an unforgettable adventure to Katmai National Park, renowned as the \"bear viewing capital of the world.\" Witness majestic brown bears in their natural habitat as they fish for salmon in Brooks Falls. Observe their incredible power and agility up close, capturing breathtaking photos and creating lasting memories. This experience offers a unique opportunity to connect with Alaska's raw wilderness and its iconic wildlife.", + "locationName": "Katmai National Park", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "alaska", + "ref": "bear-viewing-at-katmai-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_bear-viewing-at-katmai-national-park.jpg" + }, + { + "name": "Whale Watching in Juneau", + "description": "Embark on a thrilling whale-watching excursion in the waters surrounding Juneau. Marvel at the awe-inspiring sight of humpback whales breaching and tail-slapping as they migrate through these nutrient-rich waters. Keep an eye out for other marine life such as orcas, seals, and porpoises, and enjoy the stunning coastal scenery of Southeast Alaska. This activity is perfect for nature enthusiasts and wildlife lovers of all ages.", + "locationName": "Juneau", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "alaska", + "ref": "whale-watching-in-juneau", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_whale-watching-in-juneau.jpg" + }, + { + "name": "Scenic Drive on the Seward Highway", + "description": "Embark on a breathtaking road trip along the Seward Highway, one of the most scenic routes in North America. Wind through towering mountains, alongside glaciers, and past sparkling turquoise lakes. Stop at viewpoints to capture panoramic vistas and explore charming coastal towns like Seward and Girdwood. This journey offers a perfect blend of stunning landscapes, wildlife encounters, and opportunities for outdoor adventures.", + "locationName": "Seward Highway", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "alaska", + "ref": "scenic-drive-on-the-seward-highway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_scenic-drive-on-the-seward-highway.jpg" + }, + { + "name": "Visit the Anchorage Museum", + "description": "Immerse yourself in the rich history and culture of Alaska at the Anchorage Museum. Explore exhibits showcasing Alaska Native art, artifacts, and history, as well as contemporary art and science displays. Discover the stories of Alaska's people, its diverse ecosystems, and its unique place in the world. This museum offers a fascinating experience for visitors of all ages and interests.", + "locationName": "Anchorage", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "alaska", + "ref": "visit-the-anchorage-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_visit-the-anchorage-museum.jpg" + }, + { + "name": "Experience the Alaska Railroad", + "description": "Embark on a scenic journey through Alaska's wilderness aboard the historic Alaska Railroad. Choose from various routes that wind through mountains, forests, and along the coast, offering breathtaking views of glaciers, rivers, and wildlife. Relax in comfortable carriages and enjoy onboard dining while experiencing the beauty and vastness of Alaska's landscapes.", + "locationName": "Various routes throughout Alaska", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "alaska", + "ref": "experience-the-alaska-railroad", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/alaska_experience-the-alaska-railroad.jpg" + }, + { + "name": "Scenic Boat Tour", + "description": "Embark on a mesmerizing boat tour along the Amalfi Coast, taking in the stunning views of dramatic cliffs, hidden coves, and charming villages perched on the hillsides. Capture unforgettable photos and enjoy swimming in the crystal-clear turquoise waters.", + "locationName": "Amalfi Coast", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "amalfi-coast", + "ref": "scenic-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_scenic-boat-tour.jpg" + }, + { + "name": "Path of the Gods Hike", + "description": "Challenge yourself with a hike along the legendary Path of the Gods, a scenic trail offering breathtaking panoramic views of the coastline, terraced vineyards, and charming villages. Immerse yourself in the beauty of nature and experience the thrill of adventure.", + "locationName": "Agerola to Positano", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "amalfi-coast", + "ref": "path-of-the-gods-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_path-of-the-gods-hike.jpg" + }, + { + "name": "Positano Shopping Spree", + "description": "Wander through the charming streets of Positano, browsing through boutiques filled with colorful ceramics, handmade sandals, and stylish fashion. Discover unique souvenirs and indulge in the vibrant atmosphere of this picturesque town.", + "locationName": "Positano", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "amalfi-coast", + "ref": "positano-shopping-spree", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_positano-shopping-spree.jpg" + }, + { + "name": "Romantic Dinner with a View", + "description": "Treat yourself to a romantic dinner at a restaurant with breathtaking views of the Amalfi Coast. Savor delicious Italian cuisine, sip on local wine, and enjoy the enchanting ambiance as the sun sets over the horizon.", + "locationName": "Ravello or Positano", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "amalfi-coast", + "ref": "romantic-dinner-with-a-view", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_romantic-dinner-with-a-view.jpg" + }, + { + "name": "Cooking Class and Wine Tasting", + "description": "Immerse yourself in Italian culture by participating in a cooking class. Learn how to prepare traditional dishes like fresh pasta and regional specialties, and pair your culinary creations with local wines for a delightful foodie experience.", + "locationName": "Local villages or towns", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "amalfi-coast", + "ref": "cooking-class-and-wine-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_cooking-class-and-wine-tasting.jpg" + }, + { + "name": "Kayaking Adventure in the Grotta dello Smeraldo", + "description": "Embark on a mesmerizing kayaking journey to the Grotta dello Smeraldo, a hidden sea cave renowned for its emerald-green waters. Paddle through the crystal-clear waters, marvel at the unique rock formations, and discover the enchanting play of light within the cave. This unforgettable experience offers a different perspective of the Amalfi Coast's natural beauty.", + "locationName": "Grotta dello Smeraldo", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "amalfi-coast", + "ref": "kayaking-adventure-in-the-grotta-dello-smeraldo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_kayaking-adventure-in-the-grotta-dello-smeraldo.jpg" + }, + { + "name": "Vespa Tour through the Hills", + "description": "Experience the thrill of zipping through the scenic hills of the Amalfi Coast on a classic Vespa scooter. Enjoy the freedom of the open road as you wind your way through charming villages, stopping at panoramic viewpoints to capture breathtaking vistas. This adventurous tour allows you to explore hidden gems and soak up the authentic Italian atmosphere.", + "locationName": "Amalfi Coast hills", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "amalfi-coast", + "ref": "vespa-tour-through-the-hills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_vespa-tour-through-the-hills.jpg" + }, + { + "name": "Limoncello Tasting and Workshop", + "description": "Discover the secrets behind the iconic limoncello liqueur with a visit to a local limoncello producer. Learn about the traditional methods of production, from harvesting the lemons to the infusion process. Indulge in a tasting session of various limoncello flavors and even try your hand at creating your own unique blend.", + "locationName": "Local limoncello producer", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "amalfi-coast", + "ref": "limoncello-tasting-and-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_limoncello-tasting-and-workshop.jpg" + }, + { + "name": "Sunset Cruise with Swimming and Prosecco", + "description": "Set sail on a romantic sunset cruise along the Amalfi Coast. Admire the stunning coastline bathed in golden light, enjoy a refreshing swim in the turquoise waters, and sip on chilled Prosecco as you witness the breathtaking spectacle of the sun setting over the horizon. This magical experience is perfect for couples or anyone seeking a memorable evening.", + "locationName": "Amalfi Coast", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "amalfi-coast", + "ref": "sunset-cruise-with-swimming-and-prosecco", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_sunset-cruise-with-swimming-and-prosecco.jpg" + }, + { + "name": "Live Music at a Local Trattoria", + "description": "Immerse yourself in the vibrant atmosphere of a local trattoria and enjoy an evening of live music. Savor authentic Italian dishes while listening to talented musicians playing traditional and contemporary tunes. This experience offers a taste of the local culture and a chance to connect with the community.", + "locationName": "Local trattoria", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "amalfi-coast", + "ref": "live-music-at-a-local-trattoria", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_live-music-at-a-local-trattoria.jpg" + }, + { + "name": "Private Boat Trip to Capri", + "description": "Embark on a luxurious private boat trip to the stunning island of Capri. Explore the iconic Blue Grotto, swim in secluded coves, and admire the Faraglioni Rocks. Enjoy a delicious lunch onboard and soak up the Mediterranean sun.", + "locationName": "Capri", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "amalfi-coast", + "ref": "private-boat-trip-to-capri", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_private-boat-trip-to-capri.jpg" + }, + { + "name": "Hike the Sentiero degli Dei", + "description": "Challenge yourself with a hike along the Sentiero degli Dei, also known as the Path of the Gods. This scenic trail offers breathtaking panoramic views of the coastline, charming villages, and the Tyrrhenian Sea.", + "locationName": "Agerola to Nocelle", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "amalfi-coast", + "ref": "hike-the-sentiero-degli-dei", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_hike-the-sentiero-degli-dei.jpg" + }, + { + "name": "Explore the Ruins of Pompeii", + "description": "Step back in time with a visit to the ancient ruins of Pompeii, a UNESCO World Heritage Site. Witness the remarkably preserved remains of this Roman city, frozen in time by the eruption of Mount Vesuvius in 79 AD.", + "locationName": "Pompeii", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "amalfi-coast", + "ref": "explore-the-ruins-of-pompeii", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_explore-the-ruins-of-pompeii.jpg" + }, + { + "name": "Relax at a Beach Club", + "description": "Indulge in a day of relaxation at one of the many exclusive beach clubs along the Amalfi Coast. Enjoy comfortable sunbeds, refreshing cocktails, and stunning views of the sea. Some beach clubs offer water sports activities and swimming pools.", + "locationName": "Various locations along the coast", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "amalfi-coast", + "ref": "relax-at-a-beach-club", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_relax-at-a-beach-club.jpg" + }, + { + "name": "Visit the Duomo di Amalfi", + "description": "Immerse yourself in the rich history and culture of Amalfi by visiting the Duomo di Amalfi. This magnificent cathedral boasts stunning architecture, intricate mosaics, and a peaceful cloister.", + "locationName": "Amalfi", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "amalfi-coast", + "ref": "visit-the-duomo-di-amalfi", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_visit-the-duomo-di-amalfi.jpg" + }, + { + "name": "Private Yacht Excursion to Hidden Coves and Grottoes", + "description": "Embark on a luxurious private yacht excursion to discover secluded coves and hidden grottoes along the Amalfi Coast. Dive into crystal-clear waters for a refreshing swim, snorkel amidst vibrant marine life, and enjoy a gourmet lunch on board while soaking in the breathtaking coastal views. This exclusive experience promises a day of unparalleled beauty and relaxation.", + "locationName": "Amalfi Coast", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "amalfi-coast", + "ref": "private-yacht-excursion-to-hidden-coves-and-grottoes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_private-yacht-excursion-to-hidden-coves-and-grottoes.jpg" + }, + { + "name": "Ceramic Workshop in Vietri sul Mare", + "description": "Immerse yourself in the artistic heritage of the Amalfi Coast with a ceramic workshop in the charming town of Vietri sul Mare. Learn the traditional techniques of hand-painting and pottery from local artisans, and create your own unique ceramic masterpiece to take home as a souvenir. This cultural experience offers a glimpse into the region's rich artistic traditions.", + "locationName": "Vietri sul Mare", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "amalfi-coast", + "ref": "ceramic-workshop-in-vietri-sul-mare", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_ceramic-workshop-in-vietri-sul-mare.jpg" + }, + { + "name": "Wine Tasting Tour in the Hills of Tramonti", + "description": "Escape the bustling coast and venture into the tranquil hills of Tramonti for a delightful wine tasting tour. Visit family-run vineyards, learn about the unique grape varieties grown in the region, and savor a selection of exquisite local wines paired with regional cheeses and cured meats. Enjoy panoramic views of the coastline and experience the authentic charm of rural life on the Amalfi Coast.", + "locationName": "Tramonti", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "amalfi-coast", + "ref": "wine-tasting-tour-in-the-hills-of-tramonti", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_wine-tasting-tour-in-the-hills-of-tramonti.jpg" + }, + { + "name": "Guided Hike to the Torre dello Ziro Watchtower", + "description": "Embark on a scenic guided hike to the historic Torre dello Ziro watchtower, perched high above the village of Amalfi. Enjoy panoramic views of the coastline and surrounding mountains as you learn about the tower's fascinating history and its role in defending the region from invaders. This moderately challenging hike rewards you with breathtaking vistas and a sense of accomplishment.", + "locationName": "Amalfi", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "amalfi-coast", + "ref": "guided-hike-to-the-torre-dello-ziro-watchtower", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_guided-hike-to-the-torre-dello-ziro-watchtower.jpg" + }, + { + "name": "Live Like a Local: Market Visit and Home-Cooked Meal", + "description": "Experience the authentic flavors of the Amalfi Coast with a market visit and home-cooked meal. Join a local host at a vibrant market to select fresh, seasonal ingredients, and then learn to prepare traditional dishes in their home kitchen. This immersive culinary experience offers a glimpse into the daily life and culinary traditions of the region.", + "locationName": "Local villages", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "amalfi-coast", + "ref": "live-like-a-local-market-visit-and-home-cooked-meal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amalfi-coast_live-like-a-local-market-visit-and-home-cooked-meal.jpg" + }, + { + "name": "Amazon River Boat Tour", + "description": "Embark on a captivating journey down the mighty Amazon River, the lifeblood of the rainforest. Witness the vibrant ecosystem unfold before your eyes as you glide past lush vegetation, observe exotic wildlife like pink river dolphins and caimans, and encounter local communities along the riverbanks. Choose from various boat tour options, including day trips, overnight expeditions, or even multi-day cruises, to tailor your experience to your preferences.", + "locationName": "Amazon River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "amazon-rainforest", + "ref": "amazon-river-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_amazon-river-boat-tour.jpg" + }, + { + "name": "Jungle Trekking and Wildlife Spotting", + "description": "Venture deep into the heart of the Amazon rainforest on a guided jungle trek. Follow experienced local guides along winding trails, immersing yourself in the sights and sounds of the jungle. Learn about medicinal plants, spot elusive wildlife like monkeys, sloths, and colorful birds, and discover the secrets of this incredible ecosystem. Choose from various trek lengths and difficulty levels to match your fitness and adventurous spirit.", + "locationName": "Various jungle trails", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "amazon-rainforest", + "ref": "jungle-trekking-and-wildlife-spotting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_jungle-trekking-and-wildlife-spotting.jpg" + }, + { + "name": "Canopy Ziplining Adventure", + "description": "Experience the rainforest from a thrilling perspective with a canopy zipline adventure. Soar through the treetops on a network of ziplines, enjoying breathtaking views of the jungle canopy and the surrounding landscape. Feel the adrenaline rush as you glide from platform to platform, surrounded by the lush greenery and the sounds of nature. This exhilarating activity is perfect for adventure seekers and those looking for a unique way to explore the rainforest.", + "locationName": "Various zipline locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "amazon-rainforest", + "ref": "canopy-ziplining-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_canopy-ziplining-adventure.jpg" + }, + { + "name": "Indigenous Cultural Experience", + "description": "Immerse yourself in the rich cultural heritage of the Amazon's indigenous communities. Visit a local village and interact with the residents, learning about their traditional way of life, customs, and beliefs. Participate in cultural activities such as craft making, storytelling, or traditional dances. This enriching experience provides a unique opportunity to connect with the people who have called the Amazon home for centuries.", + "locationName": "Indigenous villages", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "amazon-rainforest", + "ref": "indigenous-cultural-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_indigenous-cultural-experience.jpg" + }, + { + "name": "Nighttime Wildlife Safari", + "description": "Embark on a thrilling nighttime safari to discover the nocturnal creatures of the Amazon rainforest. Accompanied by experienced guides, venture into the jungle after dark, using spotlights to search for elusive animals such as caimans, snakes, owls, and other nocturnal species. Learn about their unique adaptations and behaviors while experiencing the magic of the rainforest at night.", + "locationName": "Jungle trails", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "amazon-rainforest", + "ref": "nighttime-wildlife-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_nighttime-wildlife-safari.jpg" + }, + { + "name": "Piranha Fishing Excursion", + "description": "Embark on a thrilling fishing expedition in the Amazon River tributaries, where you'll try your hand at catching the infamous piranhas. Learn local fishing techniques and experience the excitement of reeling in these feisty fish. Knowledgeable guides will share insights about the piranhas' role in the ecosystem and their fascinating behaviors.", + "locationName": "Amazon River tributaries", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "amazon-rainforest", + "ref": "piranha-fishing-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_piranha-fishing-excursion.jpg" + }, + { + "name": "Medicinal Plants and Traditional Healing Workshop", + "description": "Delve into the rich world of traditional Amazonian medicine with a guided workshop led by indigenous healers. Discover the healing properties of native plants, learn about their uses in traditional remedies, and participate in hands-on activities like preparing herbal teas and natural ointments. Gain insights into the cultural significance of these practices and their connection to the rainforest ecosystem.", + "locationName": "Indigenous community", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "amazon-rainforest", + "ref": "medicinal-plants-and-traditional-healing-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_medicinal-plants-and-traditional-healing-workshop.jpg" + }, + { + "name": "Kayaking through Amazonian Waterways", + "description": "Paddle through the tranquil waterways of the Amazon in a kayak, immersing yourself in the sights and sounds of the rainforest. Explore hidden creeks, observe diverse wildlife from a unique perspective, and enjoy the serenity of the surrounding nature. This peaceful adventure allows you to connect with the Amazon's ecosystem at your own pace.", + "locationName": "Amazon River tributaries and flooded forests", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "amazon-rainforest", + "ref": "kayaking-through-amazonian-waterways", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_kayaking-through-amazonian-waterways.jpg" + }, + { + "name": "Birdwatching in the Rainforest Canopy", + "description": "Rise early to experience the symphony of birdsong in the Amazon rainforest canopy. Accompanied by expert birdwatching guides, embark on a hike or boat trip to prime birdwatching locations. Observe vibrant macaws, toucans, hummingbirds, and numerous other species in their natural habitat. Learn about their unique characteristics, calls, and the crucial role they play in the ecosystem.", + "locationName": "Various locations within the rainforest", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "amazon-rainforest", + "ref": "birdwatching-in-the-rainforest-canopy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_birdwatching-in-the-rainforest-canopy.jpg" + }, + { + "name": "Sunset Cruise on the Amazon River", + "description": "Embark on a magical sunset cruise along the Amazon River, soaking in the breathtaking views as the sky transforms into a canvas of vibrant colors. Enjoy the tranquil atmosphere, observe wildlife along the riverbanks, and capture stunning photographs of the unforgettable scenery. This relaxing experience provides a perfect end to a day of exploration.", + "locationName": "Amazon River", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "amazon-rainforest", + "ref": "sunset-cruise-on-the-amazon-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_sunset-cruise-on-the-amazon-river.jpg" + }, + { + "name": "Amazon River Dolphin Watching", + "description": "Embark on a magical journey to witness the playful pink river dolphins in their natural habitat. Cruise along the Amazon River tributaries, keeping an eye out for these intelligent and friendly creatures. Learn about their unique characteristics and conservation efforts while enjoying the stunning scenery of the rainforest.", + "locationName": "Amazon River tributaries", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "amazon-rainforest", + "ref": "amazon-river-dolphin-watching", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_amazon-river-dolphin-watching.jpg" + }, + { + "name": "Stargazing in the Amazon Night", + "description": "Escape the city lights and immerse yourself in the breathtaking night sky of the Amazon. Join a guided stargazing experience, where you'll learn about constellations, planets, and the Milky Way. With minimal light pollution, the Amazon offers an unforgettable opportunity to witness the celestial wonders of the Southern Hemisphere.", + "locationName": "Amazon Rainforest lodges with open clearings", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "amazon-rainforest", + "ref": "stargazing-in-the-amazon-night", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_stargazing-in-the-amazon-night.jpg" + }, + { + "name": "Amazonian Cooking Class", + "description": "Delve into the culinary traditions of the Amazon with a hands-on cooking class. Learn to prepare authentic dishes using fresh, local ingredients, such as exotic fruits, vegetables, and fish. Discover the unique flavors and techniques of Amazonian cuisine while enjoying a cultural and gastronomic adventure.", + "locationName": "Local communities or lodges", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "amazon-rainforest", + "ref": "amazonian-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_amazonian-cooking-class.jpg" + }, + { + "name": "Survival Skills Workshop", + "description": "Test your wilderness skills and learn essential survival techniques from experienced guides. Discover how to build shelters, identify edible plants, purify water, and navigate through the dense rainforest. This immersive experience will connect you with the raw beauty of the Amazon and equip you with valuable knowledge.", + "locationName": "Remote jungle locations", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "amazon-rainforest", + "ref": "survival-skills-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_survival-skills-workshop.jpg" + }, + { + "name": "Visit a Sustainable Eco-lodge", + "description": "Experience the Amazon responsibly by staying at a sustainable eco-lodge. These lodges prioritize environmental conservation and community development. Enjoy comfortable accommodations, learn about eco-friendly practices, and support local initiatives while immersing yourself in the rainforest's wonders.", + "locationName": "Various locations throughout the Amazon", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "amazon-rainforest", + "ref": "visit-a-sustainable-eco-lodge", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_visit-a-sustainable-eco-lodge.jpg" + }, + { + "name": "Caving Exploration in Gruta do Janelão", + "description": "Embark on a thrilling journey into the depths of Gruta do Janelão, one of the largest cave systems in Brazil. Marvel at the stunning geological formations, including stalactites, stalagmites, and underground rivers, and learn about the unique cave ecosystem. Experienced guides will lead you through the caverns, ensuring a safe and unforgettable adventure.", + "locationName": "Gruta do Janelão, Presidente Figueiredo", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "amazon-rainforest", + "ref": "caving-exploration-in-gruta-do-janel-o", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_caving-exploration-in-gruta-do-janel-o.jpg" + }, + { + "name": "Anavilhanas Archipelago Exploration", + "description": "Discover the Anavilhanas Archipelago, the world's largest freshwater archipelago, comprised of over 400 islands. Explore the flooded forests by boat, kayak, or canoe, and encounter diverse wildlife, including pink river dolphins, monkeys, sloths, and a variety of birds. Hike on some of the islands, swim in the clear waters, and experience the unique ecosystem of this natural wonder.", + "locationName": "Anavilhanas National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "amazon-rainforest", + "ref": "anavilhanas-archipelago-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_anavilhanas-archipelago-exploration.jpg" + }, + { + "name": "Amazonian Fruit Farm Tour and Tasting", + "description": "Indulge your taste buds in the exotic flavors of the Amazon on a fruit farm tour. Explore a local farm, learn about the cultivation of various Amazonian fruits, and sample a wide variety of fresh and delicious fruits, including acai, cupuaçu, guaraná, and many more. Discover the unique flavors and cultural significance of these fruits in the Amazonian way of life.", + "locationName": "Local fruit farms near Manaus or other Amazonian cities", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "amazon-rainforest", + "ref": "amazonian-fruit-farm-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_amazonian-fruit-farm-tour-and-tasting.jpg" + }, + { + "name": "Amazon Photography Expedition", + "description": "Capture the stunning beauty of the Amazon rainforest on a photography expedition. Join a professional photographer and guide as you explore diverse ecosystems, from the dense jungle to the riverbanks and flooded forests. Learn about wildlife photography techniques and capture breathtaking images of exotic animals, vibrant flora, and the unique landscapes of the Amazon.", + "locationName": "Various locations within the Amazon rainforest", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "amazon-rainforest", + "ref": "amazon-photography-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/amazon-rainforest_amazon-photography-expedition.jpg" + }, + { + "name": "Trek to Machu Picchu", + "description": "Embark on an unforgettable journey through the ancient Inca Trail, culminating in a breathtaking arrival at the legendary Machu Picchu. Hike through stunning mountain scenery, explore Inca ruins along the way, and witness the sunrise over the iconic lost city.", + "locationName": "Machu Picchu, Peru", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "andes-mountains", + "ref": "trek-to-machu-picchu", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_trek-to-machu-picchu.jpg" + }, + { + "name": "Explore the Salt Flats of Salar de Uyuni", + "description": "Witness the surreal beauty of the world's largest salt flats, Salar de Uyuni in Bolivia. Take a jeep tour across the vast expanse of salt, capture mind-bending perspective photos, and visit unique islands dotted with cacti.", + "locationName": "Salar de Uyuni, Bolivia", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "andes-mountains", + "ref": "explore-the-salt-flats-of-salar-de-uyuni", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_explore-the-salt-flats-of-salar-de-uyuni.jpg" + }, + { + "name": "Visit the Glaciers of Patagonia", + "description": "Venture into the southern reaches of the Andes to discover the awe-inspiring glaciers of Patagonia. Take a boat tour to witness towering icebergs calving into turquoise lakes, go trekking on a glacier, or embark on a kayaking adventure.", + "locationName": "Patagonia, Argentina/Chile", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "andes-mountains", + "ref": "visit-the-glaciers-of-patagonia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_visit-the-glaciers-of-patagonia.jpg" + }, + { + "name": "Skiing in the Andes", + "description": "Hit the slopes at world-class ski resorts nestled within the Andes Mountains. Experience thrilling downhill runs, enjoy après-ski activities, and soak in the breathtaking mountain vistas.", + "locationName": "Various ski resorts in Chile and Argentina", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "andes-mountains", + "ref": "skiing-in-the-andes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_skiing-in-the-andes.jpg" + }, + { + "name": "Cultural Encounters with Indigenous Communities", + "description": "Immerse yourself in the rich cultural heritage of the Andes by visiting indigenous communities. Learn about their traditions, crafts, and way of life, and gain a deeper appreciation for the region's history and people.", + "locationName": "Various locations throughout the Andes", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "andes-mountains", + "ref": "cultural-encounters-with-indigenous-communities", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_cultural-encounters-with-indigenous-communities.jpg" + }, + { + "name": "White Water Rafting on the Rio Apurimac", + "description": "Embark on an exhilarating white water rafting adventure on the Rio Apurimac, one of the headwaters of the Amazon River. Navigate through stunning canyons, experience the thrill of rapids, and enjoy the breathtaking scenery of the Andes Mountains.", + "locationName": "Rio Apurimac, Peru", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "andes-mountains", + "ref": "white-water-rafting-on-the-rio-apurimac", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_white-water-rafting-on-the-rio-apurimac.jpg" + }, + { + "name": "Horseback Riding in the Andean Highlands", + "description": "Explore the Andean highlands on horseback, traversing scenic trails with panoramic views of snow-capped peaks, verdant valleys, and traditional villages. Connect with nature and experience the local way of life at a relaxed pace.", + "locationName": "Andean Highlands, Ecuador", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "andes-mountains", + "ref": "horseback-riding-in-the-andean-highlands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_horseback-riding-in-the-andean-highlands.jpg" + }, + { + "name": "Stargazing in the Atacama Desert", + "description": "Venture into the Atacama Desert, one of the driest places on Earth, for an unforgettable stargazing experience. Marvel at the clarity of the night sky, observe constellations, and learn about the celestial wonders with expert guides.", + "locationName": "Atacama Desert, Chile", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "andes-mountains", + "ref": "stargazing-in-the-atacama-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_stargazing-in-the-atacama-desert.jpg" + }, + { + "name": "Mountain Biking in the Sacred Valley", + "description": "Challenge yourself with a thrilling mountain biking adventure in the Sacred Valley of Peru. Pedal through ancient Inca trails, past picturesque villages, and alongside agricultural terraces, enjoying breathtaking views of the surrounding mountains.", + "locationName": "Sacred Valley, Peru", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "andes-mountains", + "ref": "mountain-biking-in-the-sacred-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_mountain-biking-in-the-sacred-valley.jpg" + }, + { + "name": "Birdwatching in the Cloud Forests", + "description": "Immerse yourself in the biodiversity of the Andean cloud forests, home to a wide variety of bird species. Join a guided birdwatching tour and spot colorful hummingbirds, tanagers, toucans, and other fascinating avian creatures.", + "locationName": "Cloud Forests, Ecuador", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "andes-mountains", + "ref": "birdwatching-in-the-cloud-forests", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_birdwatching-in-the-cloud-forests.jpg" + }, + { + "name": "Rock Climbing in the Cordillera Blanca", + "description": "Challenge yourself with thrilling rock climbing experiences in the Cordillera Blanca, a stunning range within the Andes known for its towering granite peaks and glaciers. Whether you're a beginner or an experienced climber, there are routes for all levels, offering breathtaking views and a sense of accomplishment.", + "locationName": "Cordillera Blanca, Peru", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "andes-mountains", + "ref": "rock-climbing-in-the-cordillera-blanca", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_rock-climbing-in-the-cordillera-blanca.jpg" + }, + { + "name": "Caving in the Marble Caves", + "description": "Embark on a unique adventure exploring the Marble Caves, a series of sculpted caverns formed by waves crashing against calcium carbonate. Take a boat tour through the mesmerizing turquoise waters and marvel at the intricate patterns and formations carved into the rock over millennia.", + "locationName": "General Carrera Lake, Chile", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "andes-mountains", + "ref": "caving-in-the-marble-caves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_caving-in-the-marble-caves.jpg" + }, + { + "name": "Soak in Natural Hot Springs", + "description": "Relax and rejuvenate in the numerous natural hot springs scattered throughout the Andes. Immerse yourself in the warm, mineral-rich waters surrounded by stunning mountain scenery. Many hot springs offer additional spa treatments and wellness activities for a truly pampering experience.", + "locationName": "Various locations throughout the Andes", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "andes-mountains", + "ref": "soak-in-natural-hot-springs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_soak-in-natural-hot-springs.jpg" + }, + { + "name": "Wine Tasting in Mendoza", + "description": "Indulge in the world-renowned wines of Mendoza, Argentina, nestled at the foothills of the Andes. Visit local vineyards, tour the cellars, and savor a variety of Malbecs and other varietals while enjoying the picturesque landscapes and charming atmosphere.", + "locationName": "Mendoza, Argentina", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "andes-mountains", + "ref": "wine-tasting-in-mendoza", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_wine-tasting-in-mendoza.jpg" + }, + { + "name": "Paragliding over the Sacred Valley", + "description": "Experience the thrill of paragliding over the breathtaking Sacred Valley of the Incas in Peru. Soar through the air and enjoy panoramic views of ancient ruins, verdant fields, and snow-capped mountains. This unforgettable adventure offers a unique perspective on the region's beauty.", + "locationName": "Sacred Valley, Peru", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "andes-mountains", + "ref": "paragliding-over-the-sacred-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_paragliding-over-the-sacred-valley.jpg" + }, + { + "name": "Scenic Train Journey Through the Andes", + "description": "Embark on a breathtaking train journey through the heart of the Andes, winding past snow-capped peaks, dramatic valleys, and charming Andean villages. Enjoy panoramic views from comfortable carriages, with options for luxury experiences or budget-friendly travel. This is a perfect way to appreciate the vastness and beauty of the Andes without strenuous physical activity.", + "locationName": "Various routes throughout the Andes", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "andes-mountains", + "ref": "scenic-train-journey-through-the-andes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_scenic-train-journey-through-the-andes.jpg" + }, + { + "name": "Zip-lining in the Cloud Forest", + "description": "Experience the thrill of soaring through the lush canopy of the cloud forest on a zip-lining adventure. Enjoy breathtaking views of the surrounding mountains and valleys as you glide from platform to platform, surrounded by diverse flora and fauna. This exhilarating activity is perfect for adventure seekers and nature enthusiasts.", + "locationName": "Cloud forests in Ecuador, Peru, or Colombia", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "andes-mountains", + "ref": "zip-lining-in-the-cloud-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_zip-lining-in-the-cloud-forest.jpg" + }, + { + "name": "Exploring Colonial Towns and Cities", + "description": "Step back in time and immerse yourself in the rich history and culture of the Andes by exploring charming colonial towns and cities. Visit Cusco, the ancient Inca capital, or Quito, with its well-preserved colonial center. Discover architectural gems, vibrant markets, and museums showcasing the region's fascinating past.", + "locationName": "Cusco, Quito, and other colonial towns", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "andes-mountains", + "ref": "exploring-colonial-towns-and-cities", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_exploring-colonial-towns-and-cities.jpg" + }, + { + "name": "Indulge in Andean Gastronomy", + "description": "Embark on a culinary journey through the Andes, savoring the unique flavors and dishes of the region. Try traditional delicacies like ceviche, empanadas, and lomo saltado. Explore local markets for fresh produce and spices, or participate in a cooking class to learn the secrets of Andean cuisine.", + "locationName": "Restaurants and markets throughout the Andes", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "andes-mountains", + "ref": "indulge-in-andean-gastronomy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_indulge-in-andean-gastronomy.jpg" + }, + { + "name": "Relaxing at Thermal Baths", + "description": "Unwind and rejuvenate in the natural thermal baths scattered throughout the Andes. Immerse yourself in the soothing mineral-rich waters, surrounded by stunning mountain scenery. Many locations offer spa treatments and wellness programs for a complete relaxation experience.", + "locationName": "Various locations in the Andes, including Banos in Ecuador and Aguas Calientes in Peru", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "andes-mountains", + "ref": "relaxing-at-thermal-baths", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/andes-mountains_relaxing-at-thermal-baths.jpg" + }, + { + "name": "Sunrise at Angkor Wat", + "description": "Witness the breathtaking spectacle of sunrise over the iconic Angkor Wat temple. Arrive early to secure a prime spot near the reflecting pool and capture unforgettable photos as the sun bathes the temple in golden light.", + "locationName": "Angkor Wat", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "angkor-wat", + "ref": "sunrise-at-angkor-wat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_sunrise-at-angkor-wat.jpg" + }, + { + "name": "Explore Angkor Thom", + "description": "Embark on a journey through the ancient walled city of Angkor Thom, home to magnificent temples such as Bayon, with its enigmatic smiling faces, and the Terrace of the Elephants. Discover hidden passages, marvel at intricate carvings, and delve into the history of the Khmer Empire.", + "locationName": "Angkor Thom", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "explore-angkor-thom", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_explore-angkor-thom.jpg" + }, + { + "name": "Ta Prohm Temple and the Jungle's Embrace", + "description": "Venture into the atmospheric Ta Prohm temple, where ancient ruins are intertwined with massive tree roots. Explore the mystical atmosphere of this iconic location, featured in the film 'Tomb Raider', and imagine the power of nature reclaiming the land.", + "locationName": "Ta Prohm", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "angkor-wat", + "ref": "ta-prohm-temple-and-the-jungle-s-embrace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_ta-prohm-temple-and-the-jungle-s-embrace.jpg" + }, + { + "name": "Angkor National Museum", + "description": "Delve deeper into the history and culture of the Khmer Empire at the Angkor National Museum. Explore exhibits showcasing artifacts, sculptures, and ancient relics, providing insights into the fascinating stories behind Angkor's temples.", + "locationName": "Angkor National Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "angkor-wat", + "ref": "angkor-national-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_angkor-national-museum.jpg" + }, + { + "name": "Cambodian Cooking Class", + "description": "Immerse yourself in Cambodian culinary traditions with a hands-on cooking class. Learn to prepare authentic dishes like fish amok and Khmer curry, using fresh local ingredients and traditional techniques. Savor the flavors of your creations and gain a deeper appreciation for Cambodian cuisine.", + "locationName": "Various locations in Siem Reap", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "cambodian-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_cambodian-cooking-class.jpg" + }, + { + "name": "Banteay Srei Temple Excursion", + "description": "Embark on a journey to the enchanting Banteay Srei, known as the 'Citadel of Women.' Marvel at its intricate pink sandstone carvings and delicate details, often considered the finest example of Khmer art. Explore the temple's unique history and feminine symbolism, surrounded by a serene jungle setting.", + "locationName": "Banteay Srei Temple", + "duration": 2.5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "banteay-srei-temple-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_banteay-srei-temple-excursion.jpg" + }, + { + "name": "Quad Bike Adventure through the Countryside", + "description": "Experience the thrill of an off-road adventure on a quad bike tour through the picturesque Cambodian countryside. Traverse dirt paths, rice paddies, and local villages, immersing yourself in the rural landscapes and daily life beyond the temples. This exciting excursion offers a unique perspective of the region and its natural beauty.", + "locationName": "Angkor Wat Surrounding Countryside", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "angkor-wat", + "ref": "quad-bike-adventure-through-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_quad-bike-adventure-through-the-countryside.jpg" + }, + { + "name": "Sunset Cruise on Tonle Sap Lake", + "description": "Set sail on a tranquil sunset cruise across the vast Tonle Sap Lake, the largest freshwater lake in Southeast Asia. Witness breathtaking views as the sky transforms with vibrant colors, reflecting on the water. Observe the unique floating villages and witness the daily life of local communities who call the lake home.", + "locationName": "Tonle Sap Lake", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "sunset-cruise-on-tonle-sap-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_sunset-cruise-on-tonle-sap-lake.jpg" + }, + { + "name": "Apsara Dance Performance with Dinner", + "description": "Immerse yourself in Cambodian culture with a mesmerizing Apsara dance performance. Enjoy a traditional buffet dinner while witnessing the graceful movements and elaborate costumes of the dancers, depicting ancient myths and legends. This captivating experience offers a glimpse into the rich artistic heritage of Cambodia.", + "locationName": "Various restaurants and cultural venues in Siem Reap", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "angkor-wat", + "ref": "apsara-dance-performance-with-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_apsara-dance-performance-with-dinner.jpg" + }, + { + "name": "Phare Circus Show: A Cambodian Spectacle", + "description": "Experience the magic of Phare Circus, a captivating performance that combines acrobatics, theater, music, and storytelling. Witness the incredible talents of young Cambodian artists as they share their stories and cultural heritage through this unique and inspiring show.", + "locationName": "Phare Circus, Siem Reap", + "duration": 1.5, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "phare-circus-show-a-cambodian-spectacle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_phare-circus-show-a-cambodian-spectacle.jpg" + }, + { + "name": "Explore the Floating Villages of Kampong Phluk", + "description": "Embark on a captivating boat journey to Kampong Phluk, a unique floating village on Tonle Sap Lake. Witness the stilted houses, vibrant local life, and flooded forests, experiencing the daily rhythms of this fascinating community. Interact with villagers, learn about their fishing traditions, and savor a delicious meal at a floating restaurant.", + "locationName": "Kampong Phluk", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "explore-the-floating-villages-of-kampong-phluk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_explore-the-floating-villages-of-kampong-phluk.jpg" + }, + { + "name": "Biking Through the Cambodian Countryside", + "description": "Escape the bustling city and embark on a scenic bike tour through the picturesque Cambodian countryside. Pedal along quiet paths, passing by lush rice paddies, traditional villages, and ancient temples. Immerse yourself in the rural landscapes, interact with friendly locals, and discover hidden gems off the beaten path. This active adventure allows you to experience the authentic charm of Cambodia.", + "locationName": "Cambodian Countryside", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "angkor-wat", + "ref": "biking-through-the-cambodian-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_biking-through-the-cambodian-countryside.jpg" + }, + { + "name": "Indulge in a Traditional Khmer Massage", + "description": "Treat yourself to a rejuvenating Khmer massage, a centuries-old practice known for its therapeutic benefits. Skilled therapists use gentle stretching, acupressure, and aromatic oils to relieve tension, improve circulation, and restore balance. Choose from a variety of massage styles and durations, and let the expert hands transport you to a state of deep relaxation.", + "locationName": "Various spas and wellness centers", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "indulge-in-a-traditional-khmer-massage", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_indulge-in-a-traditional-khmer-massage.jpg" + }, + { + "name": "Phnom Kulen National Park: Waterfall and 1000 Lingas", + "description": "Embark on a day trip to Phnom Kulen National Park, a sacred mountain with stunning natural landscapes. Hike to the Kulen Waterfall, take a refreshing dip in the cool waters, and marvel at the intricate carvings of the 1000 Lingas on the riverbed. Discover hidden temples, enjoy panoramic views, and immerse yourself in the spiritual atmosphere of this natural sanctuary.", + "locationName": "Phnom Kulen National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "phnom-kulen-national-park-waterfall-and-1000-lingas", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_phnom-kulen-national-park-waterfall-and-1000-lingas.jpg" + }, + { + "name": "Pub Street: Nightlife and Culinary Delights", + "description": "As the sun sets, head to Pub Street, the vibrant heart of Siem Reap's nightlife. Explore the lively bars, restaurants, and shops, and soak up the energetic atmosphere. Sample delicious street food, enjoy live music performances, and mingle with fellow travelers. From local beers to international cocktails, Pub Street offers a diverse range of options to suit every taste.", + "locationName": "Pub Street", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "angkor-wat", + "ref": "pub-street-nightlife-and-culinary-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_pub-street-nightlife-and-culinary-delights.jpg" + }, + { + "name": "Kbal Spean: The River of a Thousand Lingas", + "description": "Embark on a jungle trek to Kbal Spean, an archaeological site known as the \"River of a Thousand Lingas.\" Discover intricate carvings of Hindu deities and symbols etched into the riverbed and waterfalls, offering a unique blend of nature and cultural heritage.", + "locationName": "Kbal Spean", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "kbal-spean-the-river-of-a-thousand-lingas", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_kbal-spean-the-river-of-a-thousand-lingas.jpg" + }, + { + "name": "Beng Mealea: The Lost Temple", + "description": "Venture off the beaten path to Beng Mealea, a sprawling temple complex enveloped by the jungle. Explore its mysterious ruins, climb over moss-covered stones, and experience the thrill of discovery in this Indiana Jones-esque adventure.", + "locationName": "Beng Mealea", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "angkor-wat", + "ref": "beng-mealea-the-lost-temple", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_beng-mealea-the-lost-temple.jpg" + }, + { + "name": "Koh Ker: The Ancient Khmer Capital", + "description": "Take a day trip to Koh Ker, a remote archaeological site that once served as the capital of the Khmer Empire. Explore the impressive Prasat Thom pyramid temple, wander through the ruins of ancient palaces, and witness the remnants of a bygone era.", + "locationName": "Koh Ker", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "angkor-wat", + "ref": "koh-ker-the-ancient-khmer-capital", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_koh-ker-the-ancient-khmer-capital.jpg" + }, + { + "name": "Hot Air Balloon Ride over Angkor Wat", + "description": "Soar above the temples of Angkor in a hot air balloon and witness the breathtaking sunrise or sunset views. Capture panoramic vistas of the sprawling complex, the surrounding countryside, and the distant Kulen Mountains.", + "locationName": "Angkor Archaeological Park", + "duration": 1.5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "angkor-wat", + "ref": "hot-air-balloon-ride-over-angkor-wat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_hot-air-balloon-ride-over-angkor-wat.jpg" + }, + { + "name": "Siem Reap Art & Culture", + "description": "Immerse yourself in the vibrant art and culture scene of Siem Reap. Visit the Angkor National Museum to delve deeper into Khmer history, explore local art galleries showcasing contemporary Cambodian artists, and attend a traditional Apsara dance performance.", + "locationName": "Siem Reap", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "angkor-wat", + "ref": "siem-reap-art-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/angkor-wat_siem-reap-art-culture.jpg" + }, + { + "name": "Guided Tour of Upper Antelope Canyon", + "description": "Embark on a mesmerizing journey through the Upper Antelope Canyon with a Navajo guide. Witness the interplay of light and shadow as sunlight filters through the narrow sandstone walls, creating breathtaking photographic opportunities. Learn about the canyon's formation, Navajo culture, and the significance of this sacred site.", + "locationName": "Upper Antelope Canyon", + "duration": 1.5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "antelope-canyon", + "ref": "guided-tour-of-upper-antelope-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_guided-tour-of-upper-antelope-canyon.jpg" + }, + { + "name": "Photography Tour of Lower Antelope Canyon", + "description": "Capture the ethereal beauty of Lower Antelope Canyon on a specialized photography tour. Accompanied by a professional photographer and Navajo guide, learn advanced techniques for photographing slot canyons and make the most of the unique lighting conditions. This tour is perfect for photography enthusiasts seeking to create stunning images.", + "locationName": "Lower Antelope Canyon", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "antelope-canyon", + "ref": "photography-tour-of-lower-antelope-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_photography-tour-of-lower-antelope-canyon.jpg" + }, + { + "name": "Scenic Drive on Highway 98", + "description": "Embark on a scenic drive along Highway 98, which winds through the Navajo Nation and offers stunning views of the surrounding landscape. Stop at viewpoints to admire the vast desert vistas, towering mesas, and distant rock formations. This drive is a great way to experience the natural beauty of the region at your own pace.", + "locationName": "Highway 98", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "antelope-canyon", + "ref": "scenic-drive-on-highway-98", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_scenic-drive-on-highway-98.jpg" + }, + { + "name": "Visit Horseshoe Bend", + "description": "Take a short hike to Horseshoe Bend, a dramatic incised meander of the Colorado River. Marvel at the sheer cliffs and the emerald-green water flowing below. Capture panoramic photos of this iconic landmark and enjoy the breathtaking views of the surrounding landscape.", + "locationName": "Horseshoe Bend", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "antelope-canyon", + "ref": "visit-horseshoe-bend", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_visit-horseshoe-bend.jpg" + }, + { + "name": "Explore the Navajo Nation", + "description": "Immerse yourself in the rich culture and history of the Navajo Nation. Visit the Navajo Nation Museum to learn about their traditions, art, and way of life. Explore local trading posts to find authentic Navajo crafts, jewelry, and artwork. Consider attending a traditional Navajo ceremony or cultural event for a deeper understanding of their heritage.", + "locationName": "Navajo Nation", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "antelope-canyon", + "ref": "explore-the-navajo-nation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_explore-the-navajo-nation.jpg" + }, + { + "name": "Stargazing in the Desert", + "description": "Experience the magic of the desert night sky with a stargazing tour. Away from city lights, Antelope Canyon offers breathtaking views of the Milky Way and constellations. Learn about Navajo astronomy and storytelling while marveling at the celestial wonders.", + "locationName": "Antelope Canyon or surrounding desert areas", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "antelope-canyon", + "ref": "stargazing-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_stargazing-in-the-desert.jpg" + }, + { + "name": "Rafting on the Colorado River", + "description": "Embark on a thrilling rafting adventure on the Colorado River, near Antelope Canyon. Navigate through scenic canyons and rapids, enjoying the stunning desert landscape from a unique perspective. Choose from various tour options depending on your desired level of intensity and duration.", + "locationName": "Colorado River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "antelope-canyon", + "ref": "rafting-on-the-colorado-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_rafting-on-the-colorado-river.jpg" + }, + { + "name": "Glamping Under the Stars", + "description": "Indulge in a unique glamping experience near Antelope Canyon. Enjoy the comfort of a luxury tent or yurt while immersing yourself in the beauty of the desert landscape. Relax by the campfire, gaze at the starlit sky, and create unforgettable memories.", + "locationName": "Glamping sites near Antelope Canyon", + "duration": 12, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "antelope-canyon", + "ref": "glamping-under-the-stars", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_glamping-under-the-stars.jpg" + }, + { + "name": "Hiking in Horseshoe Bend", + "description": "Embark on a scenic hike to Horseshoe Bend, a horseshoe-shaped incised meander of the Colorado River. Witness breathtaking panoramic views of the canyon and capture stunning photographs. This moderate hike offers a rewarding experience for nature enthusiasts.", + "locationName": "Horseshoe Bend", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "antelope-canyon", + "ref": "hiking-in-horseshoe-bend", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_hiking-in-horseshoe-bend.jpg" + }, + { + "name": "Exploring the Navajo Nation", + "description": "Immerse yourself in the rich culture and heritage of the Navajo Nation. Visit local artisans, experience traditional ceremonies, and learn about their history, traditions, and deep connection to the land. Respectful cultural tourism supports the Navajo community and provides a unique learning experience.", + "locationName": "Navajo Nation", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "antelope-canyon", + "ref": "exploring-the-navajo-nation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_exploring-the-navajo-nation.jpg" + }, + { + "name": "Mountain Biking Adventure", + "description": "Embark on an exhilarating mountain biking journey through the rugged terrain surrounding Antelope Canyon. Explore scenic trails that wind through canyons, mesas, and desert landscapes, offering breathtaking views and a thrilling off-road experience.", + "locationName": "Navajo Nation Lands", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "antelope-canyon", + "ref": "mountain-biking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_mountain-biking-adventure.jpg" + }, + { + "name": "Cultural Immersion at the Navajo Village", + "description": "Immerse yourself in the rich culture and traditions of the Navajo Nation by visiting a nearby Navajo village. Engage with local artisans, learn about their history and way of life, and witness traditional demonstrations of weaving, jewelry making, and storytelling.", + "locationName": "Navajo Village", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "antelope-canyon", + "ref": "cultural-immersion-at-the-navajo-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_cultural-immersion-at-the-navajo-village.jpg" + }, + { + "name": "Jeep Tour to Secret Canyon", + "description": "Venture beyond the well-trodden paths and discover the hidden gem of Secret Canyon. Embark on a thrilling jeep tour that takes you through rugged backcountry, revealing a secluded slot canyon adorned with stunning rock formations and unique light displays.", + "locationName": "Secret Canyon", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "antelope-canyon", + "ref": "jeep-tour-to-secret-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_jeep-tour-to-secret-canyon.jpg" + }, + { + "name": "Sunset Photography at Horseshoe Bend", + "description": "Capture the mesmerizing beauty of Horseshoe Bend at sunset with a photography tour led by a professional photographer. Learn advanced techniques for capturing the perfect shot as the golden light paints the landscape, creating a breathtaking spectacle.", + "locationName": "Horseshoe Bend", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "antelope-canyon", + "ref": "sunset-photography-at-horseshoe-bend", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_sunset-photography-at-horseshoe-bend.jpg" + }, + { + "name": "Stargazing and Astronomy Session", + "description": "Experience the magic of the desert night sky with a stargazing and astronomy session. Join a knowledgeable guide who will unveil the wonders of the cosmos, pointing out constellations, planets, and celestial phenomena, all under the blanket of a star-studded sky.", + "locationName": "Desert Surroundings", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "antelope-canyon", + "ref": "stargazing-and-astronomy-session", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_stargazing-and-astronomy-session.jpg" + }, + { + "name": "Helicopter Tour over Antelope Canyon", + "description": "Soar above the breathtaking landscapes of Antelope Canyon on an exhilarating helicopter tour. Witness the canyon's intricate formations, the vast desert expanse, and the meandering Colorado River from a unique aerial perspective. Capture panoramic photos and create unforgettable memories.", + "locationName": "Antelope Canyon and surrounding area", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "antelope-canyon", + "ref": "helicopter-tour-over-antelope-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_helicopter-tour-over-antelope-canyon.jpg" + }, + { + "name": "Visit Glen Canyon Dam and Lake Powell", + "description": "Embark on a scenic drive to Glen Canyon Dam, a marvel of engineering, and explore the picturesque Lake Powell. Take a boat tour on the lake's crystal-clear waters, surrounded by towering red rock cliffs. Enjoy swimming, fishing, or simply relaxing by the lakeshore.", + "locationName": "Glen Canyon Dam and Lake Powell", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "antelope-canyon", + "ref": "visit-glen-canyon-dam-and-lake-powell", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_visit-glen-canyon-dam-and-lake-powell.jpg" + }, + { + "name": "Explore the Vermillion Cliffs National Monument", + "description": "Discover the rugged beauty of the Vermillion Cliffs National Monument, a sprawling landscape of towering cliffs, canyons, and desert plains. Hike through scenic trails, go wildlife watching, and marvel at the unique geological formations. Keep an eye out for California condors soaring through the sky.", + "locationName": "Vermillion Cliffs National Monument", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "antelope-canyon", + "ref": "explore-the-vermillion-cliffs-national-monument", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_explore-the-vermillion-cliffs-national-monument.jpg" + }, + { + "name": "Experience Navajo Culture at a Traditional Hogan", + "description": "Immerse yourself in the rich culture of the Navajo people by visiting a traditional hogan. Learn about their history, traditions, and way of life from local guides. Participate in activities such as weaving demonstrations, storytelling sessions, and traditional food tastings.", + "locationName": "Navajo Nation", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "antelope-canyon", + "ref": "experience-navajo-culture-at-a-traditional-hogan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_experience-navajo-culture-at-a-traditional-hogan.jpg" + }, + { + "name": "Off-Roading Adventure in the Desert", + "description": "Get your adrenaline pumping with an off-roading adventure through the rugged desert terrain. Ride in a 4x4 vehicle and explore hidden canyons, sand dunes, and scenic viewpoints. This thrilling experience offers a unique way to discover the beauty of the desert landscape.", + "locationName": "Desert surrounding Antelope Canyon", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "antelope-canyon", + "ref": "off-roading-adventure-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/antelope-canyon_off-roading-adventure-in-the-desert.jpg" + }, + { + "name": "Scuba Diving Adventure", + "description": "Embark on an underwater exploration of Aruba's vibrant marine life and captivating shipwrecks. Dive into crystal-clear turquoise waters and discover a world of colorful coral reefs, tropical fish, and fascinating underwater landscapes. Experienced divers can explore the depths of the Antilla shipwreck, while beginners can enjoy shallower reefs teeming with marine life.", + "locationName": "Various dive sites around the island", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "aruba", + "ref": "scuba-diving-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_scuba-diving-adventure.jpg" + }, + { + "name": "Beach Bliss at Eagle Beach", + "description": "Relax and soak up the sun on the pristine white sands of Eagle Beach, renowned for its calm turquoise waters and breathtaking sunsets. Rent a beach umbrella and lounge chair, take a refreshing dip in the ocean, or simply unwind with a good book while enjoying the gentle sea breeze.", + "locationName": "Eagle Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "aruba", + "ref": "beach-bliss-at-eagle-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_beach-bliss-at-eagle-beach.jpg" + }, + { + "name": "Island Exploration Tour", + "description": "Embark on a thrilling off-road adventure to discover Aruba's hidden gems. Explore the rugged terrain of Arikok National Park, visit the Natural Pool, and marvel at the dramatic coastline with its crashing waves and natural bridges. This tour offers a unique perspective of Aruba's diverse landscapes.", + "locationName": "Arikok National Park and other island landmarks", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "aruba", + "ref": "island-exploration-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_island-exploration-tour.jpg" + }, + { + "name": "Sunset Sail with Cocktails", + "description": "Set sail on a romantic catamaran cruise and witness the breathtaking beauty of an Aruban sunset. Sip on tropical cocktails, enjoy the gentle sea breeze, and marvel at the vibrant colors painting the sky as the sun dips below the horizon. This experience is perfect for couples or anyone seeking a memorable evening.", + "locationName": "Departs from various locations on the island", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "aruba", + "ref": "sunset-sail-with-cocktails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_sunset-sail-with-cocktails.jpg" + }, + { + "name": "Oranjestad Cultural Exploration", + "description": "Immerse yourself in the vibrant culture of Aruba's capital city, Oranjestad. Explore the colorful Dutch colonial architecture, visit historical landmarks like Fort Zoutman, and browse the shops and art galleries for unique souvenirs. Enjoy the lively atmosphere of the local markets and indulge in delicious Caribbean cuisine.", + "locationName": "Oranjestad", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "aruba", + "ref": "oranjestad-cultural-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_oranjestad-cultural-exploration.jpg" + }, + { + "name": "Arikok National Park Adventure", + "description": "Embark on a thrilling off-road adventure through Arikok National Park, covering nearly 20% of the island. Discover hidden gems like the Natural Pool, Quadirikiri Caves, and Dos Playa. Witness stunning landscapes, diverse flora and fauna, and historical sites, making it a perfect blend of nature, culture, and adventure.", + "locationName": "Arikok National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "aruba", + "ref": "arikok-national-park-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_arikok-national-park-adventure.jpg" + }, + { + "name": "Windsurfing and Kitesurfing at Fisherman's Huts", + "description": "Experience the thrill of riding the waves at Fisherman's Huts, a renowned spot for windsurfing and kitesurfing. Whether you're a beginner or a seasoned pro, the consistent trade winds and calm waters provide the perfect conditions for an exhilarating adventure on the water. Lessons and rentals are available for all levels.", + "locationName": "Fisherman's Huts", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "aruba", + "ref": "windsurfing-and-kitesurfing-at-fisherman-s-huts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_windsurfing-and-kitesurfing-at-fisherman-s-huts.jpg" + }, + { + "name": "Foodie Delights at San Nicolas", + "description": "Embark on a culinary journey through the vibrant streets of San Nicolas, known for its artistic charm and diverse food scene. Explore local restaurants and food trucks, savoring authentic Aruban dishes like Keshi Yena and Pastechi. Don't miss the chance to indulge in fresh seafood and Caribbean-inspired cuisine, a treat for your taste buds.", + "locationName": "San Nicolas", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "aruba", + "ref": "foodie-delights-at-san-nicolas", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_foodie-delights-at-san-nicolas.jpg" + }, + { + "name": "Stargazing in the Arid Landscape", + "description": "Escape the city lights and venture into the Arikok National Park or the California Lighthouse area for an unforgettable stargazing experience. With minimal light pollution, Aruba offers breathtaking views of the night sky. Join a guided tour or find a secluded spot to marvel at the constellations and the Milky Way, a truly magical and romantic experience.", + "locationName": "Arikok National Park or California Lighthouse area", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "aruba", + "ref": "stargazing-in-the-arid-landscape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_stargazing-in-the-arid-landscape.jpg" + }, + { + "name": "Sunset Horseback Riding on the Beach", + "description": "Create unforgettable memories with a romantic horseback ride along the pristine shores of Aruba. As the sun begins its descent, enjoy the breathtaking views of the Caribbean Sea and the colorful sky. This tranquil experience is perfect for couples or anyone seeking a peaceful connection with nature and the island's beauty.", + "locationName": "Beaches of Aruba", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "aruba", + "ref": "sunset-horseback-riding-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_sunset-horseback-riding-on-the-beach.jpg" + }, + { + "name": "Kayaking the Mangrove Lagoons", + "description": "Embark on a serene kayaking adventure through the lush mangrove lagoons of Mangel Halto or Spanish Lagoon. Paddle through tranquil waters, observe the unique ecosystem, and spot colorful fish and birds. This eco-friendly activity is perfect for nature enthusiasts and offers a peaceful escape from the bustling beaches.", + "locationName": "Mangel Halto or Spanish Lagoon", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "aruba", + "ref": "kayaking-the-mangrove-lagoons", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_kayaking-the-mangrove-lagoons.jpg" + }, + { + "name": "Off-Roading through Arikok National Park", + "description": "Get your adrenaline pumping with an exhilarating off-roading tour through the rugged landscapes of Arikok National Park. Explore hidden caves, natural pools, and dramatic coastlines, while learning about the island's unique geology and history. This adventurous activity is perfect for thrill-seekers and nature lovers.", + "locationName": "Arikok National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "aruba", + "ref": "off-roading-through-arikok-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_off-roading-through-arikok-national-park.jpg" + }, + { + "name": "California Lighthouse Climb and Sunset Views", + "description": "Ascend the historic California Lighthouse for breathtaking panoramic views of the island. Witness the stunning coastline, turquoise waters, and the vibrant capital of Oranjestad. Time your visit for sunset to capture magical golden hues and create unforgettable memories.", + "locationName": "California Lighthouse", + "duration": 1, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "aruba", + "ref": "california-lighthouse-climb-and-sunset-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_california-lighthouse-climb-and-sunset-views.jpg" + }, + { + "name": "Local Flavors Food Tour", + "description": "Embark on a culinary journey and tantalize your taste buds with a local flavors food tour. Discover hidden gems and authentic eateries, savoring traditional Aruban dishes like Keshi Yena, Pastechi, and fresh seafood. This immersive experience is perfect for foodies and cultural explorers.", + "locationName": "Various locations in Oranjestad or San Nicolas", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "aruba", + "ref": "local-flavors-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_local-flavors-food-tour.jpg" + }, + { + "name": "Relaxing Spa Day and Wellness Retreat", + "description": "Escape the stresses of everyday life and indulge in a rejuvenating spa day or wellness retreat. Pamper yourself with soothing massages, revitalizing body treatments, and peaceful yoga sessions. Aruba offers a variety of luxurious spas and wellness centers where you can unwind and recharge.", + "locationName": "Various spas and wellness centers throughout the island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "aruba", + "ref": "relaxing-spa-day-and-wellness-retreat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_relaxing-spa-day-and-wellness-retreat.jpg" + }, + { + "name": "Snorkeling at Mangel Halto Reef", + "description": "Explore the vibrant underwater world of Mangel Halto Reef, a secluded haven renowned for its calm, shallow waters and abundant marine life. Swim among colorful fish, graceful sea turtles, and intricate coral formations. This accessible reef is perfect for families and snorkelers of all levels.", + "locationName": "Mangel Halto Reef", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "aruba", + "ref": "snorkeling-at-mangel-halto-reef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_snorkeling-at-mangel-halto-reef.jpg" + }, + { + "name": "Alto Vista Chapel and Noord Coast Hike", + "description": "Embark on a scenic hike along Aruba's rugged northern coast. Start at the historic Alto Vista Chapel, a peaceful landmark with panoramic ocean views. Follow the trail as it winds past dramatic cliffs, hidden coves, and natural bridges, offering breathtaking vistas at every turn. This moderate hike is perfect for nature enthusiasts and photographers.", + "locationName": "Alto Vista Chapel and Noord Coast", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "aruba", + "ref": "alto-vista-chapel-and-noord-coast-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_alto-vista-chapel-and-noord-coast-hike.jpg" + }, + { + "name": "Sunset Catamaran Cruise with Dinner", + "description": "Set sail on a romantic catamaran cruise as the sun dips below the horizon. Enjoy breathtaking views of the coastline, sip on refreshing cocktails, and savor a delicious dinner prepared on board. Dance the night away under the stars as you create unforgettable memories with your loved one.", + "locationName": "Aruba's coastline", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "aruba", + "ref": "sunset-catamaran-cruise-with-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_sunset-catamaran-cruise-with-dinner.jpg" + }, + { + "name": "Aruba Aloe Factory and Museum Tour", + "description": "Discover the secrets of Aruba's aloe vera industry with a visit to the Aruba Aloe Factory and Museum. Learn about the history of aloe cultivation on the island, witness the production process, and explore the museum's exhibits to understand the plant's medicinal properties. Don't forget to shop for aloe-infused products to take home a piece of Aruba's natural wellness.", + "locationName": "Aruba Aloe Factory and Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "aruba", + "ref": "aruba-aloe-factory-and-museum-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_aruba-aloe-factory-and-museum-tour.jpg" + }, + { + "name": "Casibari Rock Formations Exploration", + "description": "Embark on a unique adventure to the Casibari Rock Formations, a collection of giant boulders with mysterious origins. Climb to the top for panoramic views of the island, explore hidden caves and tunnels, and discover ancient pictographs left by the island's indigenous people. This natural wonder offers a glimpse into Aruba's geological past and provides a fun and educational experience for all ages.", + "locationName": "Casibari Rock Formations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "aruba", + "ref": "casibari-rock-formations-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/aruba_casibari-rock-formations-exploration.jpg" + }, + { + "name": "Explore the Biltmore Estate", + "description": "Step back in time at the Biltmore Estate, the largest privately-owned house in the United States. Explore the opulent rooms, stroll through the manicured gardens, and learn about the Vanderbilt family's legacy. You can also enjoy wine tasting at the Biltmore Winery or indulge in a farm-to-table meal at one of the estate's restaurants.", + "locationName": "Biltmore Estate", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "asheville", + "ref": "explore-the-biltmore-estate", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_explore-the-biltmore-estate.jpg" + }, + { + "name": "Hike to Breathtaking Waterfalls", + "description": "Embark on a scenic hike to one of the many stunning waterfalls surrounding Asheville. Popular choices include Looking Glass Falls, Catawba Falls, and Rainbow Falls. Immerse yourself in the beauty of the Blue Ridge Mountains and enjoy a refreshing dip in the cool mountain waters.", + "locationName": "Blue Ridge Mountains", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "asheville", + "ref": "hike-to-breathtaking-waterfalls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_hike-to-breathtaking-waterfalls.jpg" + }, + { + "name": "Discover Asheville's Craft Beer Scene", + "description": "Asheville is renowned for its thriving craft beer scene. Embark on a brewery hopping adventure and sample a wide variety of local brews. Take a guided tour to learn about the brewing process, or simply relax and enjoy a flight of beers at one of the many taprooms.", + "locationName": "Downtown Asheville and River Arts District", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "asheville", + "ref": "discover-asheville-s-craft-beer-scene", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_discover-asheville-s-craft-beer-scene.jpg" + }, + { + "name": "Immerse Yourself in Art and Culture", + "description": "Explore Asheville's vibrant arts scene. Visit the River Arts District, where you can browse through galleries and studios showcasing the works of local artists. Catch a live performance at the Asheville Community Theatre or the Diana Wortham Theatre. Don't miss the Asheville Art Museum, featuring a diverse collection of American art.", + "locationName": "River Arts District and Downtown Asheville", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "asheville", + "ref": "immerse-yourself-in-art-and-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_immerse-yourself-in-art-and-culture.jpg" + }, + { + "name": "Indulge in Farm-to-Table Delights", + "description": "Experience Asheville's thriving culinary scene. The city is known for its farm-to-table restaurants, offering fresh and locally sourced ingredients. Savor delicious dishes at renowned establishments like Curate, Rhubarb, or Posana. Explore the Asheville City Market for local produce, artisanal goods, and food trucks.", + "locationName": "Downtown Asheville and surrounding areas", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "asheville", + "ref": "indulge-in-farm-to-table-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_indulge-in-farm-to-table-delights.jpg" + }, + { + "name": "Whitewater Rafting on the French Broad River", + "description": "Experience the thrill of navigating the rapids of the French Broad River, surrounded by stunning mountain scenery. Several local outfitters offer guided whitewater rafting trips for all skill levels, from gentle family floats to adrenaline-pumping adventures.", + "locationName": "French Broad River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "asheville", + "ref": "whitewater-rafting-on-the-french-broad-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_whitewater-rafting-on-the-french-broad-river.jpg" + }, + { + "name": "Scenic Drive on the Blue Ridge Parkway", + "description": "Embark on a breathtaking journey along the Blue Ridge Parkway, renowned for its panoramic views of the Appalachian Mountains. Stop at overlooks, hiking trails, and charming towns along the way, immersing yourself in the natural beauty of the region.", + "locationName": "Blue Ridge Parkway", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "asheville", + "ref": "scenic-drive-on-the-blue-ridge-parkway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_scenic-drive-on-the-blue-ridge-parkway.jpg" + }, + { + "name": "Explore the River Arts District", + "description": "Wander through the vibrant River Arts District, home to a diverse community of artists and studios. Discover unique galleries, witness artists at work, and find one-of-a-kind pieces to take home as a souvenir of your trip.", + "locationName": "River Arts District", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "asheville", + "ref": "explore-the-river-arts-district", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_explore-the-river-arts-district.jpg" + }, + { + "name": "Visit the Asheville Botanical Gardens", + "description": "Escape the hustle and bustle of the city at the Asheville Botanical Gardens, a tranquil oasis showcasing the native flora of the Southern Appalachians. Stroll through themed gardens, admire colorful blooms, and learn about the region's unique plant life.", + "locationName": "Asheville Botanical Gardens", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "asheville", + "ref": "visit-the-asheville-botanical-gardens", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_visit-the-asheville-botanical-gardens.jpg" + }, + { + "name": "Catch a Live Music Performance", + "description": "Experience Asheville's thriving music scene by attending a live performance at one of the city's many venues. From intimate jazz clubs to outdoor amphitheaters, there's something for every musical taste, offering a taste of the local culture and talent.", + "locationName": "Various venues throughout Asheville", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "asheville", + "ref": "catch-a-live-music-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_catch-a-live-music-performance.jpg" + }, + { + "name": "Zipline Through the Forest Canopy", + "description": "Experience the thrill of soaring through the treetops on a zipline adventure. Several companies around Asheville offer exhilarating courses with varying levels of difficulty, allowing you to choose the perfect experience for your adrenaline level. Enjoy breathtaking views of the mountains and forests as you zip from platform to platform.", + "locationName": "Various locations around Asheville", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "asheville", + "ref": "zipline-through-the-forest-canopy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_zipline-through-the-forest-canopy.jpg" + }, + { + "name": "Visit the WNC Nature Center", + "description": "Get up close and personal with the diverse wildlife of the Southern Appalachians at the Western North Carolina Nature Center. This family-friendly attraction houses animals native to the region, including black bears, red wolves, otters, and birds of prey. Learn about conservation efforts and the importance of protecting these fascinating creatures.", + "locationName": "Western North Carolina Nature Center", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "asheville", + "ref": "visit-the-wnc-nature-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_visit-the-wnc-nature-center.jpg" + }, + { + "name": "Explore the Grove Arcade", + "description": "Step back in time and wander through the historic Grove Arcade, a beautifully restored 1920s shopping center. Admire the stunning architecture, browse unique boutiques and art galleries, and enjoy a delicious meal at one of the many restaurants.", + "locationName": "Grove Arcade", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "asheville", + "ref": "explore-the-grove-arcade", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_explore-the-grove-arcade.jpg" + }, + { + "name": "Take a Ghost Tour", + "description": "Discover the spooky side of Asheville with a spine-tingling ghost tour. Learn about the city's haunted history as you visit historic sites and hear tales of paranormal activity. Several companies offer a variety of tours, from family-friendly options to more intense experiences for thrill-seekers.", + "locationName": "Various locations in downtown Asheville", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "asheville", + "ref": "take-a-ghost-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_take-a-ghost-tour.jpg" + }, + { + "name": "Enjoy a Relaxing Spa Day", + "description": "Unwind and rejuvenate with a luxurious spa day at one of Asheville's many wellness retreats. Indulge in a massage, facial, or body treatment, and let the stresses of everyday life melt away. Several spas offer packages that include access to saunas, steam rooms, and other amenities.", + "locationName": "Various spas and wellness centers throughout Asheville", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "asheville", + "ref": "enjoy-a-relaxing-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_enjoy-a-relaxing-spa-day.jpg" + }, + { + "name": "Go Mountain Biking on World-Class Trails", + "description": "Experience the thrill of mountain biking on some of the best trails in the East Coast. From beginner-friendly paths to challenging single-tracks, Asheville offers a variety of options for all skill levels. Rent a bike or join a guided tour to explore the scenic beauty of the Blue Ridge Mountains.", + "locationName": "Bent Creek Experimental Forest, Tsali Recreation Area", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "asheville", + "ref": "go-mountain-biking-on-world-class-trails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_go-mountain-biking-on-world-class-trails.jpg" + }, + { + "name": "Take a Hot Air Balloon Ride at Sunrise", + "description": "Soar above the stunning landscapes of Asheville in a hot air balloon and witness the breathtaking colors of sunrise over the Blue Ridge Mountains. Enjoy a peaceful and unforgettable experience as you float gently through the sky.", + "locationName": "Asheville Hot Air Balloons", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "asheville", + "ref": "take-a-hot-air-balloon-ride-at-sunrise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_take-a-hot-air-balloon-ride-at-sunrise.jpg" + }, + { + "name": "Visit the Folk Art Center", + "description": "Discover the rich traditions of Appalachian craft and folk art at the Folk Art Center. Explore a diverse collection of handmade crafts, including pottery, textiles, woodcarvings, and more. You can also watch demonstrations, attend workshops, and find unique souvenirs.", + "locationName": "Folk Art Center", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "asheville", + "ref": "visit-the-folk-art-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_visit-the-folk-art-center.jpg" + }, + { + "name": "Embark on a Culinary Adventure", + "description": "Indulge in Asheville's thriving culinary scene with a food tour or cooking class. Explore the city's diverse restaurants, from farm-to-table bistros to ethnic eateries. Learn about the local food culture and savor delicious dishes made with fresh, seasonal ingredients.", + "locationName": "Asheville Food Tours, The Asheville Kitchen", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "asheville", + "ref": "embark-on-a-culinary-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_embark-on-a-culinary-adventure.jpg" + }, + { + "name": "Go Stargazing in the Mountains", + "description": "Escape the city lights and experience the magic of stargazing in the pristine night sky of the Blue Ridge Mountains. Join a guided astronomy tour or find a secluded spot to marvel at the constellations and celestial wonders.", + "locationName": "Blue Ridge Observatory, Mount Mitchell State Park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "asheville", + "ref": "go-stargazing-in-the-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/asheville_go-stargazing-in-the-mountains.jpg" + }, + { + "name": "Hike to the Crater Lakes of Sete Cidades", + "description": "Embark on a scenic hike to the iconic Sete Cidades Massif, home to twin crater lakes - one green, one blue. Witness breathtaking panoramic views of the volcanic caldera and surrounding landscapes. The trail offers varying difficulty levels, catering to both casual walkers and experienced hikers.", + "locationName": "Sete Cidades", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "azores", + "ref": "hike-to-the-crater-lakes-of-sete-cidades", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_hike-to-the-crater-lakes-of-sete-cidades.jpg" + }, + { + "name": "Whale and Dolphin Watching Excursion", + "description": "Set sail on an unforgettable boat tour to witness the majestic marine life of the Azores. Encounter various whale species, playful dolphins, and other fascinating creatures in their natural habitat. Knowledgeable guides provide insights into the region's rich biodiversity.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "azores", + "ref": "whale-and-dolphin-watching-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_whale-and-dolphin-watching-excursion.jpg" + }, + { + "name": "Explore the Historic City of Angra do Heroísmo", + "description": "Wander through the charming streets of Angra do Heroísmo, a UNESCO World Heritage Site. Admire the colorful colonial architecture, visit historic forts and churches, and soak up the vibrant atmosphere of this cultural hub. Discover local shops, cafes, and restaurants offering Azorean specialties.", + "locationName": "Angra do Heroísmo", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "azores", + "ref": "explore-the-historic-city-of-angra-do-hero-smo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_explore-the-historic-city-of-angra-do-hero-smo.jpg" + }, + { + "name": "Relax in the Terra Nostra Park and Thermal Baths", + "description": "Immerse yourself in the lush beauty of Terra Nostra Park, renowned for its diverse botanical collection and serene ambiance. Take a dip in the iron-rich thermal waters of the park's historic pool, known for their therapeutic properties. Enjoy the tranquility of nature and unwind in this idyllic setting.", + "locationName": "Furnas", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "azores", + "ref": "relax-in-the-terra-nostra-park-and-thermal-baths", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_relax-in-the-terra-nostra-park-and-thermal-baths.jpg" + }, + { + "name": "Canyoning Adventure in Ribeira dos Caldeirões", + "description": "Experience an adrenaline-pumping canyoning adventure in the stunning Ribeira dos Caldeirões Natural Park. Descend cascading waterfalls, rappel down rocky cliffs, and swim through crystal-clear pools. This thrilling activity is perfect for adventure seekers and nature enthusiasts.", + "locationName": "Ribeira dos Caldeirões Natural Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "azores", + "ref": "canyoning-adventure-in-ribeira-dos-caldeir-es", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_canyoning-adventure-in-ribeira-dos-caldeir-es.jpg" + }, + { + "name": "Go spelunking in the Algar do Carvão lava cave", + "description": "Embark on an underground adventure by exploring the Algar do Carvão, a unique lava cave formed thousands of years ago. Descend into the depths of the earth and marvel at the geological formations, including stalactites, stalagmites, and a crystal-clear lake.", + "locationName": "Terceira Island", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "azores", + "ref": "go-spelunking-in-the-algar-do-carv-o-lava-cave", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_go-spelunking-in-the-algar-do-carv-o-lava-cave.jpg" + }, + { + "name": "Take a boat tour to Vila Franca do Campo Islet", + "description": "Escape to a picturesque islet off the coast of São Miguel. Take a boat tour to Vila Franca do Campo Islet, a volcanic crater turned natural swimming pool. Relax on the small beach, swim in the turquoise waters, or go snorkeling to discover the vibrant marine life.", + "locationName": "São Miguel Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "azores", + "ref": "take-a-boat-tour-to-vila-franca-do-campo-islet", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_take-a-boat-tour-to-vila-franca-do-campo-islet.jpg" + }, + { + "name": "Savor the local flavors on a food tour", + "description": "Indulge in the Azores' unique gastronomy with a guided food tour. Sample regional specialties like cozido das Furnas (a stew cooked in volcanic steam), fresh seafood, and locally produced cheeses and wines. Discover the islands' culinary heritage and savor the authentic flavors.", + "locationName": "Various Islands", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "azores", + "ref": "savor-the-local-flavors-on-a-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_savor-the-local-flavors-on-a-food-tour.jpg" + }, + { + "name": "Go horseback riding through scenic landscapes", + "description": "Explore the Azores' stunning landscapes on horseback. Ride through rolling hills, lush forests, and alongside dramatic coastlines. Enjoy the tranquility of nature and experience the islands from a different perspective. This activity is suitable for all skill levels.", + "locationName": "Various Islands", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "azores", + "ref": "go-horseback-riding-through-scenic-landscapes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_go-horseback-riding-through-scenic-landscapes.jpg" + }, + { + "name": "Tee off at a world-class golf course", + "description": "Challenge yourself on one of the Azores' renowned golf courses. Enjoy breathtaking ocean views and lush green landscapes as you perfect your swing. Several islands offer championship-level courses designed by famous architects, providing an unforgettable golfing experience.", + "locationName": "São Miguel Island and Terceira Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "azores", + "ref": "tee-off-at-a-world-class-golf-course", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_tee-off-at-a-world-class-golf-course.jpg" + }, + { + "name": "Kayaking Along the Coast", + "description": "Embark on a serene kayaking adventure along the stunning Azorean coastline. Paddle through crystal-clear waters, explore hidden coves and sea caves, and witness the dramatic cliffs and volcanic formations from a unique perspective. Keep an eye out for playful dolphins and other marine life that call these waters home. This activity is suitable for various skill levels and offers a peaceful way to connect with nature.", + "locationName": "Various coastal locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "azores", + "ref": "kayaking-along-the-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_kayaking-along-the-coast.jpg" + }, + { + "name": "Birdwatching in the Azores", + "description": "The Azores are a paradise for birdwatchers, with a diverse range of endemic and migratory species. Join a guided tour or venture out on your own to spot unique birds like the Azores bullfinch, Monteiro's storm petrel, and the Cory's shearwater. Explore the islands' varied habitats, from lush forests to volcanic peaks, and witness the incredible avian diversity of this archipelago.", + "locationName": "Various locations across the islands", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "azores", + "ref": "birdwatching-in-the-azores", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_birdwatching-in-the-azores.jpg" + }, + { + "name": "Stargazing on a Volcanic Peak", + "description": "Escape the city lights and experience the magic of stargazing on one of the Azores' volcanic peaks. The remote location and minimal light pollution offer breathtaking views of the night sky. Join a guided astronomy tour or simply lay back and marvel at the constellations, planets, and the Milky Way stretching across the darkness.", + "locationName": "Mount Pico or other volcanic peaks", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "azores", + "ref": "stargazing-on-a-volcanic-peak", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_stargazing-on-a-volcanic-peak.jpg" + }, + { + "name": "Scuba Diving or Snorkeling in Underwater Volcanic Landscapes", + "description": "Dive into the underwater world of the Azores and discover a mesmerizing volcanic landscape beneath the waves. Explore unique rock formations, swim through underwater arches, and encounter a variety of marine life, including colorful fish, octopuses, and sea turtles. Whether you choose scuba diving or snorkeling, this experience offers an unforgettable glimpse into the archipelago's hidden depths.", + "locationName": "Various dive sites around the islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "azores", + "ref": "scuba-diving-or-snorkeling-in-underwater-volcanic-landscapes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_scuba-diving-or-snorkeling-in-underwater-volcanic-landscapes.jpg" + }, + { + "name": "Explore the Vineyards and Wineries", + "description": "Embark on a delightful journey through the Azores' unique wine culture. Visit local vineyards, learn about the distinctive grape varieties grown in the volcanic soil, and indulge in wine tastings at charming wineries. Discover the secrets behind the islands' renowned Verdelho wines and savor the flavors of this special terroir.", + "locationName": "Pico Island or other islands with vineyards", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "azores", + "ref": "explore-the-vineyards-and-wineries", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_explore-the-vineyards-and-wineries.jpg" + }, + { + "name": "Swim with Wild Dolphins", + "description": "Embark on an unforgettable adventure swimming alongside wild dolphins in their natural habitat. Experienced guides will lead you to the best spots for encountering these playful creatures, allowing you to witness their incredible agility and grace firsthand. This eco-friendly activity ensures responsible interaction with the dolphins, creating a truly magical and respectful experience.", + "locationName": "Atlantic Ocean surrounding the Azores", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "azores", + "ref": "swim-with-wild-dolphins", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_swim-with-wild-dolphins.jpg" + }, + { + "name": "Jeep Tour to Lagoa do Fogo", + "description": "Embark on a thrilling off-road adventure in a 4x4 jeep to reach the breathtaking Lagoa do Fogo, a stunning crater lake nestled within the Água de Pau Massif stratovolcano. Traverse rugged terrains and lush landscapes, enjoying panoramic views along the way. Once at the lake, take a refreshing dip in its crystal-clear waters or hike to a nearby viewpoint for breathtaking vistas of the surrounding volcanic caldera.", + "locationName": "Lagoa do Fogo", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "azores", + "ref": "jeep-tour-to-lagoa-do-fogo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_jeep-tour-to-lagoa-do-fogo.jpg" + }, + { + "name": "Explore the Gruta do Natal", + "description": "Embark on a captivating journey through the Gruta do Natal, a lava cave formed thousands of years ago by volcanic activity. Discover a subterranean world adorned with unique rock formations, stalactites, and stalagmites as you delve into the depths of the Earth. Learn about the geological history and formation of the cave from knowledgeable guides, making this a fascinating and educational experience.", + "locationName": "Gruta do Natal", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "azores", + "ref": "explore-the-gruta-do-natal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_explore-the-gruta-do-natal.jpg" + }, + { + "name": "Indulge in Azorean Gastronomy at a Local Restaurant", + "description": "Immerse yourself in the rich culinary traditions of the Azores by dining at a local restaurant. Savor the flavors of fresh seafood, locally sourced meats, and regional specialties like Cozido das Furnas, a stew slow-cooked in volcanic steam. Pair your meal with a glass of Azorean wine, known for its unique volcanic terroir, and enjoy the warm hospitality of the local people.", + "locationName": "Various restaurants throughout the Azores", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "azores", + "ref": "indulge-in-azorean-gastronomy-at-a-local-restaurant", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_indulge-in-azorean-gastronomy-at-a-local-restaurant.jpg" + }, + { + "name": "Take a Surfing Lesson", + "description": "Catch some waves and experience the thrill of surfing in the Atlantic Ocean. Whether you're a beginner or an experienced surfer, the Azores offers a variety of surf breaks suitable for all skill levels. Take a lesson from a qualified instructor who will guide you through the basics or help you refine your technique. Enjoy the exhilaration of riding the waves while surrounded by the stunning coastal scenery.", + "locationName": "Various beaches throughout the Azores", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "azores", + "ref": "take-a-surfing-lesson", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/azores_take-a-surfing-lesson.jpg" + }, + { + "name": "Sunrise Hike at Mount Batur", + "description": "Embark on an unforgettable adventure with an early morning trek up Mount Batur, an active volcano. Witness the breathtaking sunrise over the island, painting the sky with vibrant hues while enjoying panoramic views of the surrounding landscape. This challenging yet rewarding experience is perfect for adventure enthusiasts and nature lovers.", + "locationName": "Mount Batur", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "bali", + "ref": "sunrise-hike-at-mount-batur", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_sunrise-hike-at-mount-batur.jpg" + }, + { + "name": "Ubud Monkey Forest", + "description": "Immerse yourself in the lush greenery of the Ubud Monkey Forest, a sanctuary home to hundreds of playful monkeys. Observe these fascinating creatures in their natural habitat, swinging through the trees and interacting with visitors. Explore the ancient temples within the forest, adding a touch of cultural exploration to your wildlife encounter.", + "locationName": "Ubud Monkey Forest", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bali", + "ref": "ubud-monkey-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_ubud-monkey-forest.jpg" + }, + { + "name": "Tanah Lot Temple at Sunset", + "description": "Witness the iconic Tanah Lot Temple, perched on a rocky outcrop overlooking the ocean. Visit during the golden hours of sunset for a truly magical experience as the sky transforms into a canvas of warm colors, creating a mesmerizing backdrop for this sacred landmark.", + "locationName": "Tanah Lot Temple", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "bali", + "ref": "tanah-lot-temple-at-sunset", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_tanah-lot-temple-at-sunset.jpg" + }, + { + "name": "Traditional Balinese Cooking Class", + "description": "Delve into the rich culinary heritage of Bali by joining a traditional cooking class. Learn the secrets of preparing authentic Balinese dishes, from fragrant spices to fresh local ingredients. Master the art of creating flavorful classics like Nasi Goreng and Satay, and bring home newfound skills to impress your friends and family.", + "locationName": "Various locations in Ubud and other towns", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "bali", + "ref": "traditional-balinese-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_traditional-balinese-cooking-class.jpg" + }, + { + "name": "Relaxing Spa Day", + "description": "Indulge in a day of pampering and rejuvenation at one of Bali's many luxurious spas. Choose from a wide range of treatments, including traditional Balinese massages, body scrubs, and flower baths. Unwind amidst tranquil surroundings, allowing the expert therapists to melt away your stress and leave you feeling refreshed and revitalized.", + "locationName": "Various spas throughout Bali", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "bali", + "ref": "relaxing-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_relaxing-spa-day.jpg" + }, + { + "name": "Tegallalang Rice Terraces", + "description": "Immerse yourself in the emerald landscapes of the Tegallalang Rice Terraces. Wander through the iconic, tiered rice paddies, capture stunning photos, and witness the traditional Subak irrigation system in action. Explore local craft shops and art stalls for unique souvenirs.", + "locationName": "Tegallalang", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "bali", + "ref": "tegallalang-rice-terraces", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_tegallalang-rice-terraces.jpg" + }, + { + "name": "Waterfall Adventure", + "description": "Embark on a refreshing journey to one of Bali's breathtaking waterfalls. Hike through lush rainforests, swim in natural pools beneath cascading falls, and enjoy the tranquility of nature. Popular choices include Sekumpul Waterfall, Gitgit Waterfall, and Tegenungan Waterfall.", + "locationName": "Various locations across Bali", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bali", + "ref": "waterfall-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_waterfall-adventure.jpg" + }, + { + "name": "Uluwatu Cliffside Kecak Dance", + "description": "Witness the captivating Kecak dance performance at Uluwatu Temple. As the sun sets over the Indian Ocean, be mesmerized by the rhythmic chanting and intricate movements of the dancers, telling the story of the Ramayana against a dramatic clifftop backdrop.", + "locationName": "Uluwatu Temple", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "bali", + "ref": "uluwatu-cliffside-kecak-dance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_uluwatu-cliffside-kecak-dance.jpg" + }, + { + "name": "Nusa Islands Day Trip", + "description": "Escape to the idyllic Nusa Islands, just a short boat ride away from mainland Bali. Explore the pristine beaches of Nusa Lembongan, Nusa Ceningan, and Nusa Penida. Go snorkeling or diving among vibrant coral reefs, relax on white sand shores, and discover hidden coves and dramatic cliffs.", + "locationName": "Nusa Islands", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "bali", + "ref": "nusa-islands-day-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_nusa-islands-day-trip.jpg" + }, + { + "name": "Seminyak Shopping and Nightlife", + "description": "Indulge in a vibrant evening in Seminyak. Explore the trendy boutiques and designer stores for fashion, art, and homeware. Savor delicious cuisine at world-class restaurants and experience the lively nightlife scene with beach clubs, bars, and live music venues.", + "locationName": "Seminyak", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "bali", + "ref": "seminyak-shopping-and-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_seminyak-shopping-and-nightlife.jpg" + }, + { + "name": "White Water Rafting on the Ayung River", + "description": "Embark on an exhilarating adventure through Bali's lush landscapes with a white water rafting experience on the Ayung River. Navigate thrilling rapids, surrounded by stunning scenery, and feel the rush of adrenaline as you paddle through the cascading waters. This activity is perfect for adventure seekers and nature enthusiasts.", + "locationName": "Ayung River", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "bali", + "ref": "white-water-rafting-on-the-ayung-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_white-water-rafting-on-the-ayung-river.jpg" + }, + { + "name": "Cycling Through Rural Villages", + "description": "Escape the tourist crowds and immerse yourself in the authentic charm of Bali's countryside with a leisurely bike ride through rural villages. Pedal past verdant rice paddies, traditional houses, and friendly locals, experiencing the island's peaceful way of life. This activity offers a unique perspective on Balinese culture and is suitable for all ages.", + "locationName": "Jatiluwih or Sidemen", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bali", + "ref": "cycling-through-rural-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_cycling-through-rural-villages.jpg" + }, + { + "name": "Dolphin Watching Tour in Lovina", + "description": "Witness the playful beauty of dolphins in their natural habitat with a dolphin watching tour in Lovina. Embark on an early morning boat ride and marvel as these intelligent creatures leap and frolic in the waves. Capture unforgettable memories and enjoy the stunning coastal scenery of northern Bali.", + "locationName": "Lovina Beach", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "bali", + "ref": "dolphin-watching-tour-in-lovina", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_dolphin-watching-tour-in-lovina.jpg" + }, + { + "name": "Sunset Cruise with Dinner", + "description": "Indulge in a romantic and unforgettable evening with a sunset cruise along Bali's coastline. Sail across the glistening waters, enjoying breathtaking views of the setting sun as it paints the sky with vibrant colors. Savor a delicious dinner on board and create lasting memories with your loved ones.", + "locationName": "Benoa Harbor", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "bali", + "ref": "sunset-cruise-with-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_sunset-cruise-with-dinner.jpg" + }, + { + "name": "Traditional Balinese Dance Performance", + "description": "Immerse yourself in the rich cultural heritage of Bali by attending a traditional Balinese dance performance. Be mesmerized by the graceful movements, vibrant costumes, and captivating storytelling of dances like Legong, Barong, or Kecak. This experience offers a deeper understanding of Balinese art and traditions.", + "locationName": "Ubud Palace or Uluwatu Temple", + "duration": 1.5, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "bali", + "ref": "traditional-balinese-dance-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_traditional-balinese-dance-performance.jpg" + }, + { + "name": "Snorkeling or Diving at Menjangan Island", + "description": "Explore the underwater paradise of Menjangan Island, known for its pristine coral reefs and diverse marine life. Snorkel or dive amidst colorful fish, turtles, and even reef sharks, discovering the vibrant ecosystem of the Bali Barat National Park.", + "locationName": "Menjangan Island", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "bali", + "ref": "snorkeling-or-diving-at-menjangan-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_snorkeling-or-diving-at-menjangan-island.jpg" + }, + { + "name": "Sunrise Trek to Mount Agung", + "description": "Embark on a challenging yet rewarding trek to the summit of Mount Agung, Bali's highest volcano. Witness a breathtaking sunrise over the island, with panoramic views stretching across the landscape to the ocean.", + "locationName": "Mount Agung", + "duration": 8, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "bali", + "ref": "sunrise-trek-to-mount-agung", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_sunrise-trek-to-mount-agung.jpg" + }, + { + "name": "Spiritual Cleansing at Tirta Empul Temple", + "description": "Immerse yourself in Balinese Hindu culture with a spiritual cleansing ritual at Tirta Empul Temple. Bathe in the holy spring water, believed to possess healing properties, and participate in traditional prayer ceremonies for a unique cultural experience.", + "locationName": "Tirta Empul Temple", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bali", + "ref": "spiritual-cleansing-at-tirta-empul-temple", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_spiritual-cleansing-at-tirta-empul-temple.jpg" + }, + { + "name": "Authentic Market Experience at Ubud Art Market", + "description": "Wander through the bustling Ubud Art Market, a treasure trove of Balinese handicrafts, textiles, and souvenirs. Practice your bargaining skills, discover unique local crafts, and find the perfect memento to remember your trip.", + "locationName": "Ubud Art Market", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bali", + "ref": "authentic-market-experience-at-ubud-art-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_authentic-market-experience-at-ubud-art-market.jpg" + }, + { + "name": "Romantic Dinner with Sunset Views at Jimbaran Bay", + "description": "Indulge in a romantic dinner on the beach at Jimbaran Bay. Savor fresh seafood barbeque as you watch the sun dip below the horizon, painting the sky with vibrant colors. Enjoy live music and the sound of waves crashing for an unforgettable evening.", + "locationName": "Jimbaran Bay", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "bali", + "ref": "romantic-dinner-with-sunset-views-at-jimbaran-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bali_romantic-dinner-with-sunset-views-at-jimbaran-bay.jpg" + }, + { + "name": "Lake Louise and Moraine Lake", + "description": "Embark on a scenic journey to the iconic Lake Louise and Moraine Lake. Marvel at the turquoise waters reflecting the surrounding mountains and glaciers. Enjoy a leisurely stroll along the lakeshore, rent a canoe for a peaceful paddle, or challenge yourself with a hike to a panoramic viewpoint. Don't miss the chance to capture breathtaking photos of these natural wonders.", + "locationName": "Lake Louise and Moraine Lake", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "lake-louise-and-moraine-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_lake-louise-and-moraine-lake.jpg" + }, + { + "name": "Banff Gondola and Sulphur Mountain", + "description": "Ascend Sulphur Mountain in the Banff Gondola for awe-inspiring views of the surrounding peaks and valleys. At the summit, explore the interpretive center, enjoy a meal with a view at the restaurant, or hike the boardwalk to Sanson's Peak for an even more expansive panorama. Keep an eye out for wildlife, such as bighorn sheep and marmots.", + "locationName": "Sulphur Mountain", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "banff-national-park", + "ref": "banff-gondola-and-sulphur-mountain", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_banff-gondola-and-sulphur-mountain.jpg" + }, + { + "name": "Johnston Canyon Hike", + "description": "Embark on a moderate hike through Johnston Canyon, following a series of catwalks and bridges alongside cascading waterfalls and crystal-clear pools. Admire the Lower Falls and continue to the Upper Falls for an even more impressive display. This family-friendly trail offers a refreshing escape into nature.", + "locationName": "Johnston Canyon", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "banff-national-park", + "ref": "johnston-canyon-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_johnston-canyon-hike.jpg" + }, + { + "name": "Banff Upper Hot Springs", + "description": "Relax and rejuvenate in the soothing mineral waters of the Banff Upper Hot Springs. Surrounded by stunning mountain scenery, these natural hot springs offer a therapeutic experience. Enjoy the warm waters and let your cares melt away while taking in the fresh mountain air.", + "locationName": "Banff Upper Hot Springs", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "banff-upper-hot-springs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_banff-upper-hot-springs.jpg" + }, + { + "name": "Wildlife Watching Tour", + "description": "Join a guided wildlife watching tour to increase your chances of spotting Banff's diverse wildlife. Look for elk, deer, bighorn sheep, bears, and other animals in their natural habitat. Learn about the park's ecosystem and conservation efforts from knowledgeable guides.", + "locationName": "Various locations within Banff National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "banff-national-park", + "ref": "wildlife-watching-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_wildlife-watching-tour.jpg" + }, + { + "name": "Scenic Drive on the Icefields Parkway", + "description": "Embark on a breathtaking journey along the Icefields Parkway, renowned as one of the world's most scenic drives. Marvel at the majestic glaciers, cascading waterfalls, and towering mountains that adorn the landscape. Keep an eye out for wildlife like bears, elk, and bighorn sheep along the way. Make stops at iconic viewpoints such as Bow Lake and Peyto Lake for unforgettable photo opportunities.", + "locationName": "Icefields Parkway", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "scenic-drive-on-the-icefields-parkway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_scenic-drive-on-the-icefields-parkway.jpg" + }, + { + "name": "Whitewater Rafting on the Kicking Horse River", + "description": "Experience the thrill of whitewater rafting on the Kicking Horse River, known for its exhilarating rapids and stunning canyon scenery. Join a guided tour and navigate through the rushing waters, surrounded by the beauty of the Canadian Rockies. Choose from various trip lengths and intensity levels to suit your adventurous spirit.", + "locationName": "Kicking Horse River", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "banff-national-park", + "ref": "whitewater-rafting-on-the-kicking-horse-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_whitewater-rafting-on-the-kicking-horse-river.jpg" + }, + { + "name": "Explore the Cave and Basin National Historic Site", + "description": "Delve into the history of Banff National Park at the Cave and Basin National Historic Site, where Canada's first national park was established. Explore the natural hot springs that were discovered in the late 19th century, learn about the area's indigenous heritage, and visit the interactive exhibits that showcase the park's ecological and cultural significance.", + "locationName": "Cave and Basin National Historic Site", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "explore-the-cave-and-basin-national-historic-site", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_explore-the-cave-and-basin-national-historic-site.jpg" + }, + { + "name": "Indulge in a Relaxing Spa Day", + "description": "Unwind and rejuvenate with a luxurious spa day at one of Banff's renowned wellness retreats. Treat yourself to a massage, facial, or body treatment, and soak in the soothing mineral pools surrounded by breathtaking mountain views. Many spas offer a range of packages and services to suit your preferences, ensuring a truly blissful experience.", + "locationName": "Various spas in Banff", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "banff-national-park", + "ref": "indulge-in-a-relaxing-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_indulge-in-a-relaxing-spa-day.jpg" + }, + { + "name": "Discover the Charm of Banff Avenue", + "description": "Stroll along Banff Avenue, the heart of the town, and immerse yourself in its vibrant atmosphere. Browse through unique boutiques, art galleries, and souvenir shops, or indulge in a delicious meal at one of the many restaurants offering diverse cuisines. Enjoy live music performances, street entertainment, and the lively ambiance of this charming mountain town.", + "locationName": "Banff Avenue", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "discover-the-charm-of-banff-avenue", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_discover-the-charm-of-banff-avenue.jpg" + }, + { + "name": "Stargazing in the Dark Sky Preserve", + "description": "Experience the awe-inspiring beauty of the night sky in Banff National Park, a designated Dark Sky Preserve. Join a guided stargazing tour or venture out on your own to marvel at the constellations, planets, and Milky Way, away from the light pollution of the town.", + "locationName": "Various locations within Banff National Park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "stargazing-in-the-dark-sky-preserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_stargazing-in-the-dark-sky-preserve.jpg" + }, + { + "name": "Horseback Riding through the Mountains", + "description": "Embark on a horseback riding adventure through the stunning landscapes of Banff National Park. Several outfitters offer guided tours for all skill levels, allowing you to explore scenic trails, meadows, and forests while enjoying the fresh mountain air and breathtaking views.", + "locationName": "Various locations within Banff National Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "banff-national-park", + "ref": "horseback-riding-through-the-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_horseback-riding-through-the-mountains.jpg" + }, + { + "name": "Canoeing or Kayaking on Lake Minnewanka", + "description": "Rent a canoe or kayak and explore the pristine waters of Lake Minnewanka, the largest lake in Banff National Park. Paddle along the shoreline, surrounded by towering mountains, and keep an eye out for wildlife such as bald eagles, ospreys, and bighorn sheep.", + "locationName": "Lake Minnewanka", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "canoeing-or-kayaking-on-lake-minnewanka", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_canoeing-or-kayaking-on-lake-minnewanka.jpg" + }, + { + "name": "Scenic Helicopter Tour over the Rockies", + "description": "Take to the skies for a breathtaking helicopter tour over the Canadian Rockies. Soar above glaciers, turquoise lakes, and snow-capped peaks, enjoying panoramic views of Banff National Park and the surrounding wilderness. This unforgettable experience offers a unique perspective on the vastness and beauty of the region.", + "locationName": "Various helicopter tour operators in Banff", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "banff-national-park", + "ref": "scenic-helicopter-tour-over-the-rockies", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_scenic-helicopter-tour-over-the-rockies.jpg" + }, + { + "name": "Explore the Town of Banff", + "description": "Wander through the charming town of Banff, with its unique shops, art galleries, and museums. Discover local crafts and souvenirs, enjoy delicious cuisine at one of the many restaurants, or visit the Whyte Museum of the Canadian Rockies to delve into the region's history and culture.", + "locationName": "Town of Banff", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "explore-the-town-of-banff", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_explore-the-town-of-banff.jpg" + }, + { + "name": "Via Ferrata Climb", + "description": "Experience the thrill of climbing the Rocky Mountains on a guided Via Ferrata excursion. With iron rungs, suspension bridges, and breathtaking views, this adventure offers a unique perspective of the park's stunning landscapes. Choose from various routes catering to different skill levels, making it an exciting option for adventurous travelers.", + "locationName": "Mount Norquay", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "banff-national-park", + "ref": "via-ferrata-climb", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_via-ferrata-climb.jpg" + }, + { + "name": "Columbia Icefield Adventure", + "description": "Embark on a once-in-a-lifetime journey to the Columbia Icefield, where you can walk on the Athabasca Glacier and learn about its fascinating history and geology. Explore the Icefields Parkway, stopping at stunning viewpoints like Peyto Lake, and experience the Glacier Skywalk, a glass-floored platform offering panoramic mountain vistas.", + "locationName": "Columbia Icefield", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "banff-national-park", + "ref": "columbia-icefield-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_columbia-icefield-adventure.jpg" + }, + { + "name": "Lake Minnewanka Boat Cruise", + "description": "Discover the beauty of Lake Minnewanka on a scenic boat cruise. Learn about the lake's history, legends, and geological formations while enjoying breathtaking views of the surrounding mountains and forests. Keep an eye out for wildlife such as bighorn sheep, elk, and bald eagles.", + "locationName": "Lake Minnewanka", + "duration": 1.5, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "lake-minnewanka-boat-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_lake-minnewanka-boat-cruise.jpg" + }, + { + "name": "Banff Gondola and Sky Bistro Dining Experience", + "description": "Ascend Sulphur Mountain on the Banff Gondola and savor a delectable meal at the Sky Bistro. Enjoy panoramic views of the surrounding peaks, valleys, and the town of Banff while indulging in a culinary experience that combines local ingredients with breathtaking scenery.", + "locationName": "Sulphur Mountain", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "banff-national-park", + "ref": "banff-gondola-and-sky-bistro-dining-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_banff-gondola-and-sky-bistro-dining-experience.jpg" + }, + { + "name": "Johnston Canyon Icewalk", + "description": "Embark on a winter wonderland adventure with a guided icewalk through Johnston Canyon. Discover frozen waterfalls, ice caves, and stunning ice formations while learning about the canyon's geology and winter ecology. This unique experience offers a magical perspective of the park's winter landscape.", + "locationName": "Johnston Canyon", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "banff-national-park", + "ref": "johnston-canyon-icewalk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/banff-national-park_johnston-canyon-icewalk.jpg" + }, + { + "name": "Scuba Diving in the Great Blue Hole", + "description": "Embark on an unforgettable underwater adventure by diving into the iconic Great Blue Hole, a massive underwater sinkhole renowned for its crystal-clear waters and diverse marine life. Explore the unique geological formations and encounter various species of sharks, tropical fish, and colorful coral reefs.", + "locationName": "Great Blue Hole", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "belize", + "ref": "scuba-diving-in-the-great-blue-hole", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_scuba-diving-in-the-great-blue-hole.jpg" + }, + { + "name": "Exploring Ancient Maya Ruins", + "description": "Step back in time and discover the mysteries of the ancient Maya civilization by visiting the impressive archaeological sites of Caracol and Xunantunich. Climb towering temples, explore intricate carvings, and learn about the fascinating history and culture of this once-thriving civilization.", + "locationName": "Caracol or Xunantunich", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "belize", + "ref": "exploring-ancient-maya-ruins", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_exploring-ancient-maya-ruins.jpg" + }, + { + "name": "Cave Tubing Adventure", + "description": "Embark on a thrilling cave tubing adventure through the lush Belizean rainforest. Float along underground rivers, marvel at stunning stalactites and stalagmites, and experience the unique ecosystem of these hidden caves.", + "locationName": " Caves Branch River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "belize", + "ref": "cave-tubing-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_cave-tubing-adventure.jpg" + }, + { + "name": "Wildlife Watching in the Jungle", + "description": "Immerse yourself in the vibrant biodiversity of the Belizean jungle. Embark on a guided wildlife tour and encounter fascinating creatures like howler monkeys, toucans, jaguars, and tapirs. Learn about the delicate ecosystem and conservation efforts in place to protect these incredible animals.", + "locationName": "Cockscomb Basin Wildlife Sanctuary", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "belize", + "ref": "wildlife-watching-in-the-jungle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_wildlife-watching-in-the-jungle.jpg" + }, + { + "name": "Relaxing on the Beaches", + "description": "Unwind and soak up the sun on the pristine beaches of Belize. Choose from secluded coves, lively stretches of sand, or private islands. Enjoy swimming, snorkeling, kayaking, or simply relaxing with a refreshing drink and enjoying the breathtaking ocean views.", + "locationName": "Ambergris Caye or Caye Caulker", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "belize", + "ref": "relaxing-on-the-beaches", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_relaxing-on-the-beaches.jpg" + }, + { + "name": "Horseback Riding through the Jungle", + "description": "Embark on a thrilling horseback riding adventure through the lush jungles of Belize. Traverse scenic trails, encounter exotic wildlife, and discover hidden waterfalls. This unique experience offers a different perspective of the Belizean rainforest, allowing you to connect with nature in a memorable way.", + "locationName": "Mayan Mountains or Mountain Pine Ridge Forest Reserve", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "belize", + "ref": "horseback-riding-through-the-jungle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_horseback-riding-through-the-jungle.jpg" + }, + { + "name": "Kayaking on the Belize River", + "description": "Paddle your way through the heart of Belize on a scenic kayaking tour down the Belize River. Witness diverse birdlife, lush vegetation, and perhaps even crocodiles sunning on the banks. This relaxing yet adventurous activity is perfect for nature enthusiasts and offers a glimpse into the country's natural beauty.", + "locationName": "Belize River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "belize", + "ref": "kayaking-on-the-belize-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_kayaking-on-the-belize-river.jpg" + }, + { + "name": "Sunset Sailing and Snorkeling", + "description": "Experience the magic of a Belizean sunset while sailing along the Caribbean Sea. As the sky transforms into a canvas of vibrant colors, enjoy snorkeling amidst colorful coral reefs and tropical fish. This romantic and unforgettable activity combines relaxation, adventure, and breathtaking natural beauty.", + "locationName": "Ambergris Caye or Caye Caulker", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "belize", + "ref": "sunset-sailing-and-snorkeling", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_sunset-sailing-and-snorkeling.jpg" + }, + { + "name": "Culinary Tour and Cooking Class", + "description": "Delve into the vibrant flavors of Belizean cuisine with a culinary tour and cooking class. Visit local markets to discover fresh ingredients, learn traditional cooking techniques from experienced chefs, and savor the delicious dishes you create. This immersive experience is perfect for food enthusiasts and offers a taste of Belizean culture.", + "locationName": "San Ignacio or Placencia", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "belize", + "ref": "culinary-tour-and-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_culinary-tour-and-cooking-class.jpg" + }, + { + "name": "Ziplining through the Rainforest Canopy", + "description": "Soar through the Belizean rainforest canopy on an exhilarating ziplining adventure. Experience breathtaking views, feel the adrenaline rush, and witness the jungle from a unique perspective. This thrilling activity is perfect for adventure seekers and offers an unforgettable experience.", + "locationName": "Mayflower Bocawina National Park or Calico Jack's Village", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "belize", + "ref": "ziplining-through-the-rainforest-canopy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_ziplining-through-the-rainforest-canopy.jpg" + }, + { + "name": "Birding in Crooked Tree Wildlife Sanctuary", + "description": "Embark on a serene journey through the Crooked Tree Wildlife Sanctuary, a haven for bird enthusiasts. With over 300 species of birds, including Jabiru storks, egrets, and ospreys, this sanctuary offers exceptional birdwatching opportunities. Explore the lagoons, creeks, and savannas by boat or kayak, immersing yourself in the sights and sounds of Belize's avian wonders.", + "locationName": "Crooked Tree Wildlife Sanctuary", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "belize", + "ref": "birding-in-crooked-tree-wildlife-sanctuary", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_birding-in-crooked-tree-wildlife-sanctuary.jpg" + }, + { + "name": "ATM Cave Spelunking", + "description": "Embark on an exhilarating adventure through Actun Tunichil Muknal (ATM) Cave, a sacred Maya archaeological site. Hike through the jungle, swim through underground rivers, and marvel at ancient artifacts, including skeletal remains and pottery, while learning about Maya rituals and beliefs.", + "locationName": "Actun Tunichil Muknal (ATM) Cave", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "belize", + "ref": "atm-cave-spelunking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_atm-cave-spelunking.jpg" + }, + { + "name": "Chocolate Making Workshop", + "description": "Indulge your senses in the rich history and flavors of Belizean chocolate at a chocolate-making workshop. Learn about the ancient Maya cacao traditions, participate in the bean-to-bar process, and create your own delicious chocolate treats to savor.", + "locationName": "Various locations in Belize", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "belize", + "ref": "chocolate-making-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_chocolate-making-workshop.jpg" + }, + { + "name": "Garifuna Cultural Experience", + "description": "Immerse yourself in the vibrant Garifuna culture on the southern coast of Belize. Visit Hopkins or Dangriga to witness traditional drumming and dance performances, savor authentic Garifuna cuisine, and learn about their unique history, language, and customs.", + "locationName": "Hopkins or Dangriga", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "belize", + "ref": "garifuna-cultural-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_garifuna-cultural-experience.jpg" + }, + { + "name": "Cockscomb Basin Wildlife Sanctuary Hiking", + "description": "Embark on a thrilling hiking expedition through the Cockscomb Basin Wildlife Sanctuary, the world's first jaguar preserve. Explore diverse trails, encounter exotic wildlife such as monkeys, tapirs, and birds, and immerse yourself in the beauty of the Belizean rainforest.", + "locationName": "Cockscomb Basin Wildlife Sanctuary", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "belize", + "ref": "cockscomb-basin-wildlife-sanctuary-hiking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_cockscomb-basin-wildlife-sanctuary-hiking.jpg" + }, + { + "name": "Island Hopping and Snorkeling Adventure", + "description": "Embark on a boat tour to explore the stunning islands of Belize. Discover the vibrant coral reefs and diverse marine life while snorkeling in crystal-clear turquoise waters. Visit idyllic islands like Ambergris Caye and Caye Caulker, soaking up the sun and enjoying the laid-back island vibes.", + "locationName": "Belize Barrier Reef and Islands", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "belize", + "ref": "island-hopping-and-snorkeling-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_island-hopping-and-snorkeling-adventure.jpg" + }, + { + "name": "Fishing in the Caribbean Sea", + "description": "Cast your line and experience the thrill of deep-sea fishing in the Caribbean Sea. Target a variety of fish species, including marlin, tuna, and dorado. Enjoy the scenic beauty of the open water and the challenge of reeling in your catch.", + "locationName": "Caribbean Sea", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "belize", + "ref": "fishing-in-the-caribbean-sea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_fishing-in-the-caribbean-sea.jpg" + }, + { + "name": "Stand-Up Paddleboarding on Placencia Lagoon", + "description": "Glide along the calm waters of Placencia Lagoon on a stand-up paddleboard. Enjoy the peaceful surroundings and observe the local wildlife, including mangroves, birds, and marine life. This relaxing activity is suitable for all skill levels.", + "locationName": "Placencia Lagoon", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "belize", + "ref": "stand-up-paddleboarding-on-placencia-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_stand-up-paddleboarding-on-placencia-lagoon.jpg" + }, + { + "name": "Mayan Cooking Class and Cultural Immersion", + "description": "Delve into the rich culinary traditions of Belize with a Mayan cooking class. Learn to prepare traditional dishes using local ingredients and ancient cooking techniques. Immerse yourself in the Mayan culture and gain insights into their history and way of life.", + "locationName": "San Antonio Village", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "belize", + "ref": "mayan-cooking-class-and-cultural-immersion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_mayan-cooking-class-and-cultural-immersion.jpg" + }, + { + "name": "Nighttime Bioluminescence Tour", + "description": "Experience the magical phenomenon of bioluminescence on a nighttime boat tour. Witness the water come alive with sparkling blue light as you paddle through the lagoon. Learn about the organisms that create this natural wonder and enjoy the enchanting atmosphere.", + "locationName": "Anderson Lagoon", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "belize", + "ref": "nighttime-bioluminescence-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/belize_nighttime-bioluminescence-tour.jpg" + }, + { + "name": "Hike to Tiger's Nest Monastery", + "description": "Embark on a breathtaking hike through pine forests and rhododendron blooms to reach the iconic Tiger's Nest Monastery, clinging precariously to a cliffside. Witness panoramic views of the Paro Valley and immerse yourself in the spiritual aura of this sacred site.", + "locationName": "Paro Valley", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "bhutan", + "ref": "hike-to-tiger-s-nest-monastery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_hike-to-tiger-s-nest-monastery.jpg" + }, + { + "name": "Explore the Punakha Dzong", + "description": "Step into the architectural wonder of Punakha Dzong, a majestic fortress at the confluence of two rivers. Marvel at its intricate woodwork, vibrant murals, and serene courtyards. Learn about its historical significance as the former seat of the Bhutanese government.", + "locationName": "Punakha", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bhutan", + "ref": "explore-the-punakha-dzong", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_explore-the-punakha-dzong.jpg" + }, + { + "name": "Attend a Tsechu Festival", + "description": "Immerse yourself in the vibrant cultural spectacle of a Tsechu festival, where masked dancers perform traditional folk dances and religious ceremonies. Witness the colorful costumes, lively music, and the deep-rooted spirituality of the Bhutanese people.", + "locationName": "Various locations throughout Bhutan", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bhutan", + "ref": "attend-a-tsechu-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_attend-a-tsechu-festival.jpg" + }, + { + "name": "Visit the National Museum of Bhutan", + "description": "Delve into the rich history and cultural heritage of Bhutan at the National Museum. Explore exhibits showcasing ancient artifacts, traditional costumes, religious relics, and contemporary art. Gain insights into the unique way of life in this Himalayan kingdom.", + "locationName": "Paro", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "bhutan", + "ref": "visit-the-national-museum-of-bhutan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_visit-the-national-museum-of-bhutan.jpg" + }, + { + "name": "Meditate at a Buddhist Monastery", + "description": "Find inner peace and serenity with a meditation session at a serene Buddhist monastery. Learn about Buddhist philosophy, practice mindfulness techniques, and soak in the tranquil atmosphere of these spiritual sanctuaries.", + "locationName": "Various monasteries throughout Bhutan", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 1, + "destinationRef": "bhutan", + "ref": "meditate-at-a-buddhist-monastery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_meditate-at-a-buddhist-monastery.jpg" + }, + { + "name": "White Water Rafting on the Mo Chhu River", + "description": "Experience the thrill of white water rafting down the Mo Chhu River, surrounded by stunning Himalayan scenery. Navigate through rapids with experienced guides, enjoying the adrenaline rush and breathtaking views. This activity is perfect for adventure seekers and nature lovers.", + "locationName": "Punakha Valley", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "bhutan", + "ref": "white-water-rafting-on-the-mo-chhu-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_white-water-rafting-on-the-mo-chhu-river.jpg" + }, + { + "name": "Soak in a Traditional Hot Stone Bath", + "description": "Indulge in a relaxing and therapeutic hot stone bath, a unique Bhutanese tradition. Immerse yourself in a wooden tub filled with mineral-rich water and heated stones, believed to have healing properties. This experience is perfect for unwinding after a day of exploring.", + "locationName": "Paro Valley", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "bhutan", + "ref": "soak-in-a-traditional-hot-stone-bath", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_soak-in-a-traditional-hot-stone-bath.jpg" + }, + { + "name": "Attend a Local Archery Tournament", + "description": "Witness the national sport of Bhutan, archery, at a local tournament. Observe the skilled archers compete in this traditional and vibrant event, where you can experience the excitement and cultural significance of the sport.", + "locationName": "Thimphu", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "bhutan", + "ref": "attend-a-local-archery-tournament", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_attend-a-local-archery-tournament.jpg" + }, + { + "name": "Hike to the Khamsum Yulley Namgyal Chorten", + "description": "Embark on a scenic hike to the Khamsum Yulley Namgyal Chorten, a stunning temple perched on a ridge overlooking the Punakha Valley. Enjoy panoramic views of the surrounding landscapes and explore the intricate architecture and spiritual significance of the chorten.", + "locationName": "Punakha Valley", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "bhutan", + "ref": "hike-to-the-khamsum-yulley-namgyal-chorten", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_hike-to-the-khamsum-yulley-namgyal-chorten.jpg" + }, + { + "name": "Learn the Art of Bhutanese Cuisine", + "description": "Delve into the flavors of Bhutanese cuisine by participating in a cooking class. Learn to prepare traditional dishes like ema datshi (chili and cheese) and momos (dumplings), using local ingredients and techniques. This experience is perfect for food enthusiasts and those seeking a cultural immersion.", + "locationName": "Thimphu or Paro", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "bhutan", + "ref": "learn-the-art-of-bhutanese-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_learn-the-art-of-bhutanese-cuisine.jpg" + }, + { + "name": "Witness the Paro Tshechu", + "description": "Immerse yourself in the vibrant spectacle of the Paro Tshechu, a religious festival featuring masked dances, colorful costumes, and traditional music. Witness the unique Cham dances performed by monks, believed to ward off evil spirits and bring blessings.", + "locationName": "Paro Dzong", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bhutan", + "ref": "witness-the-paro-tshechu", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_witness-the-paro-tshechu.jpg" + }, + { + "name": "Birdwatching in Phobjikha Valley", + "description": "Embark on a serene birdwatching expedition in the picturesque Phobjikha Valley, home to the endangered black-necked cranes. Witness these majestic birds in their natural habitat, along with other Himalayan species, surrounded by breathtaking mountain scenery.", + "locationName": "Phobjikha Valley", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "bhutan", + "ref": "birdwatching-in-phobjikha-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_birdwatching-in-phobjikha-valley.jpg" + }, + { + "name": "Shop for Handicrafts in Thimphu", + "description": "Discover the unique craftsmanship of Bhutan at the bustling Thimphu market. Browse through a variety of handmade textiles, thangkas (Buddhist paintings), woodcarvings, and other souvenirs. Find the perfect memento to take home and support local artisans.", + "locationName": "Thimphu Market", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "bhutan", + "ref": "shop-for-handicrafts-in-thimphu", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_shop-for-handicrafts-in-thimphu.jpg" + }, + { + "name": "Experience Farm Life in a Rural Homestay", + "description": "Escape the city and immerse yourself in the tranquility of rural Bhutan with a homestay experience. Participate in daily farm activities, learn about traditional agricultural practices, and savor authentic Bhutanese cuisine prepared with fresh, local ingredients.", + "locationName": "Rural villages near Paro or Punakha", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bhutan", + "ref": "experience-farm-life-in-a-rural-homestay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_experience-farm-life-in-a-rural-homestay.jpg" + }, + { + "name": "Hike to Tango Monastery", + "description": "Embark on a scenic hike to the Tango Monastery, perched on a cliffside overlooking the Thimphu Valley. Enjoy panoramic views of the surrounding mountains and visit the monastery, a significant center for Buddhist studies.", + "locationName": "Tango Monastery", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "bhutan", + "ref": "hike-to-tango-monastery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_hike-to-tango-monastery.jpg" + }, + { + "name": "Paragliding over the Himalayas", + "description": "Soar like a bird and witness breathtaking panoramic views of the Himalayas, verdant valleys, and ancient monasteries. Experience the thrill of paragliding with experienced pilots, offering tandem flights for all skill levels.", + "locationName": "Paro Valley", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "bhutan", + "ref": "paragliding-over-the-himalayas", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_paragliding-over-the-himalayas.jpg" + }, + { + "name": "Mountain Biking through Scenic Trails", + "description": "Embark on an exhilarating mountain biking adventure through Bhutan's rugged terrains and picturesque landscapes. Explore remote villages, pedal past terraced fields, and challenge yourself with thrilling downhill descents.", + "locationName": "Punakha Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bhutan", + "ref": "mountain-biking-through-scenic-trails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_mountain-biking-through-scenic-trails.jpg" + }, + { + "name": "Wildlife Safari in Royal Manas National Park", + "description": "Discover the rich biodiversity of Bhutan's oldest national park, Royal Manas. Embark on a wildlife safari to spot endangered species like Bengal tigers, Asian elephants, one-horned rhinoceros, and various bird species in their natural habitat.", + "locationName": "Royal Manas National Park", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "bhutan", + "ref": "wildlife-safari-in-royal-manas-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_wildlife-safari-in-royal-manas-national-park.jpg" + }, + { + "name": "Camping under the Starry Sky", + "description": "Escape the city lights and immerse yourself in the tranquility of Bhutan's pristine wilderness. Set up camp amidst stunning mountain scenery, enjoy bonfires under the starry sky, and wake up to the sounds of nature.", + "locationName": "Bumthang Valley", + "duration": 12, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "bhutan", + "ref": "camping-under-the-starry-sky", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_camping-under-the-starry-sky.jpg" + }, + { + "name": "Discover the Art of Thangka Painting", + "description": "Delve into the intricate world of Bhutanese art by learning the traditional Thangka painting technique. Join a workshop led by skilled artisans and create your own masterpiece, gaining insights into Buddhist symbolism and artistic expression.", + "locationName": "Thimphu", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bhutan", + "ref": "discover-the-art-of-thangka-painting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bhutan_discover-the-art-of-thangka-painting.jpg" + }, + { + "name": "Explore Hawaii Volcanoes National Park", + "description": "Embark on a journey through volcanic landscapes at Hawaii Volcanoes National Park. Witness the awe-inspiring Kilauea volcano, hike through lava tubes, and learn about the fascinating geological history of the island. Don't miss the Thurston Lava Tube and the Kilauea Iki Crater Overlook for breathtaking views.", + "locationName": "Hawaii Volcanoes National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-island-hawaii", + "ref": "explore-hawaii-volcanoes-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_explore-hawaii-volcanoes-national-park.jpg" + }, + { + "name": "Snorkel with Manta Rays at Night", + "description": "Experience the magic of swimming alongside gentle manta rays at night. Join a guided tour to Manta Ray Night Snorkel spot, where these graceful creatures gather to feed on plankton. Witness their impressive size and graceful movements as they glide through the illuminated waters.", + "locationName": "Manta Ray Night Snorkel", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "big-island-hawaii", + "ref": "snorkel-with-manta-rays-at-night", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_snorkel-with-manta-rays-at-night.jpg" + }, + { + "name": "Hike to the Top of Mauna Kea for Stargazing", + "description": "Embark on a memorable adventure to the summit of Mauna Kea, a dormant volcano and one of the best stargazing locations on Earth. Join a guided tour or drive yourself to the visitor center, then marvel at the breathtaking panoramic views and the celestial wonders above.", + "locationName": "Mauna Kea", + "duration": 5, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "big-island-hawaii", + "ref": "hike-to-the-top-of-mauna-kea-for-stargazing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_hike-to-the-top-of-mauna-kea-for-stargazing.jpg" + }, + { + "name": "Relax on Black Sand Beaches", + "description": "Indulge in the unique beauty of Punalu'u Black Sand Beach, where volcanic activity has created a stunning contrast of black sand and turquoise waters. Relax on the beach, watch for sea turtles, and take a refreshing dip in the ocean.", + "locationName": "Punalu'u Black Sand Beach", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "big-island-hawaii", + "ref": "relax-on-black-sand-beaches", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_relax-on-black-sand-beaches.jpg" + }, + { + "name": "Visit Akaka Falls State Park", + "description": "Immerse yourself in the lush rainforest scenery of Akaka Falls State Park. Hike through the park's trails, surrounded by vibrant vegetation and cascading waterfalls. Witness the impressive 442-foot Akaka Falls, a true natural wonder.", + "locationName": "Akaka Falls State Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-island-hawaii", + "ref": "visit-akaka-falls-state-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_visit-akaka-falls-state-park.jpg" + }, + { + "name": "Horseback Riding Adventure", + "description": "Embark on a thrilling horseback riding adventure through the scenic landscapes of the Big Island. Explore lush rainforests, volcanic trails, and panoramic ocean views as you connect with nature and experience the island's beauty from a unique perspective. This activity is suitable for all skill levels, offering a memorable experience for both novice and experienced riders.", + "locationName": "Various ranches and stables across the island", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-island-hawaii", + "ref": "horseback-riding-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_horseback-riding-adventure.jpg" + }, + { + "name": "Waipio Valley Expedition", + "description": "Journey into the heart of the Big Island's dramatic Waipio Valley, often referred to as the 'Valley of the Kings.' Embark on a 4x4 tour or hike down the steep valley walls to discover cascading waterfalls, black sand beaches, and ancient Hawaiian taro fields. Immerse yourself in the rich history and breathtaking scenery of this secluded paradise.", + "locationName": "Waipio Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-island-hawaii", + "ref": "waipio-valley-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_waipio-valley-expedition.jpg" + }, + { + "name": "Coffee Farm Tour and Tasting", + "description": "Delve into the world of Kona coffee with a captivating tour of a local coffee farm. Learn about the bean-to-cup process, from cultivation and harvesting to roasting and brewing. Indulge in a delightful tasting session, savoring the unique flavors and aromas of freshly brewed Kona coffee.", + "locationName": "Kona coffee belt", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-island-hawaii", + "ref": "coffee-farm-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_coffee-farm-tour-and-tasting.jpg" + }, + { + "name": "Sunset Sail along the Kona Coast", + "description": "Set sail on a romantic sunset cruise along the picturesque Kona Coast. As the sun dips below the horizon, casting a golden glow over the Pacific Ocean, enjoy breathtaking views of the coastline, volcanic peaks, and marine life. Savor delicious appetizers and cocktails while creating unforgettable memories.", + "locationName": "Kona Coast", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "big-island-hawaii", + "ref": "sunset-sail-along-the-kona-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_sunset-sail-along-the-kona-coast.jpg" + }, + { + "name": "Hawaii Tropical Botanical Garden", + "description": "Escape into a world of tropical wonder at the Hawaii Tropical Botanical Garden. Wander through lush pathways, discovering a diverse collection of over 2,000 exotic plant species. Admire cascading waterfalls, vibrant flowers, and towering trees, immersing yourself in the beauty and tranquility of this natural oasis.", + "locationName": "Onomea Bay", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-island-hawaii", + "ref": "hawaii-tropical-botanical-garden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_hawaii-tropical-botanical-garden.jpg" + }, + { + "name": "Kayaking and Dolphin Watching at Kealakekua Bay", + "description": "Embark on a serene kayaking adventure in the crystal-clear waters of Kealakekua Bay, a marine sanctuary teeming with life. Paddle along the scenic coastline, explore hidden coves, and encounter playful spinner dolphins in their natural habitat. This eco-friendly activity offers breathtaking views and a chance to connect with nature's wonders.", + "locationName": "Kealakekua Bay", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-island-hawaii", + "ref": "kayaking-and-dolphin-watching-at-kealakekua-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_kayaking-and-dolphin-watching-at-kealakekua-bay.jpg" + }, + { + "name": "Swim with Sea Turtles at Punalu'u Black Sand Beach", + "description": "Experience the unique beauty of Punalu'u Black Sand Beach, where endangered Hawaiian green sea turtles bask on the volcanic shoreline. Take a refreshing dip in the ocean and snorkel alongside these gentle giants. Witnessing these majestic creatures up close is an unforgettable experience.", + "locationName": "Punalu'u Black Sand Beach", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-island-hawaii", + "ref": "swim-with-sea-turtles-at-punalu-u-black-sand-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_swim-with-sea-turtles-at-punalu-u-black-sand-beach.jpg" + }, + { + "name": "Zipline Through the Rainforest Canopy", + "description": "Embark on an exhilarating zipline adventure through the lush rainforest canopy. Soar above the trees, enjoying panoramic views of the island's diverse landscapes. With multiple zipline courses available, ranging from beginner to advanced, this activity is perfect for thrill-seekers and nature enthusiasts alike.", + "locationName": "Hilo or Kapoho", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "big-island-hawaii", + "ref": "zipline-through-the-rainforest-canopy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_zipline-through-the-rainforest-canopy.jpg" + }, + { + "name": "Explore the Thurston Lava Tube", + "description": "Venture into the depths of the earth and explore the Thurston Lava Tube, a natural volcanic cave formed centuries ago. Walk through the dimly lit tunnel, marvel at the unique geological formations, and learn about the island's volcanic history. This fascinating adventure offers a glimpse into the powerful forces that shaped the Big Island.", + "locationName": "Hawaii Volcanoes National Park", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "big-island-hawaii", + "ref": "explore-the-thurston-lava-tube", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_explore-the-thurston-lava-tube.jpg" + }, + { + "name": "Attend a Traditional Hawaiian Luau", + "description": "Immerse yourself in Hawaiian culture at a traditional luau. Feast on a delicious buffet of local cuisine, enjoy live music and hula performances, and learn about the island's rich history and traditions. This vibrant and festive experience is a perfect way to celebrate your Hawaiian vacation.", + "locationName": "Various locations throughout the island", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-island-hawaii", + "ref": "attend-a-traditional-hawaiian-luau", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_attend-a-traditional-hawaiian-luau.jpg" + }, + { + "name": "Helicopter Tour Over Volcanoes", + "description": "Experience the breathtaking beauty and raw power of the Big Island's volcanic landscapes from a unique perspective on a thrilling helicopter tour. Soar above active craters, witness lava flows cascading into the ocean, and marvel at the dramatic contrast between volcanic rock and lush rainforests.", + "locationName": "Various helicopter tour operators on the Big Island", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "big-island-hawaii", + "ref": "helicopter-tour-over-volcanoes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_helicopter-tour-over-volcanoes.jpg" + }, + { + "name": "Stargazing at Mauna Kea Observatories", + "description": "Embark on a celestial journey to the summit of Mauna Kea, home to some of the world's most advanced astronomical observatories. Join a guided stargazing tour and peer through powerful telescopes to witness the wonders of the night sky, from distant galaxies to the rings of Saturn.", + "locationName": "Mauna Kea Observatories", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-island-hawaii", + "ref": "stargazing-at-mauna-kea-observatories", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_stargazing-at-mauna-kea-observatories.jpg" + }, + { + "name": "Scuba Diving in Kealakekua Bay", + "description": "Dive into the crystal-clear waters of Kealakekua Bay, a marine sanctuary teeming with colorful coral reefs, tropical fish, and playful dolphins. Explore underwater lava tubes, encounter majestic sea turtles, and discover the vibrant marine ecosystem that makes this bay a renowned diving destination.", + "locationName": "Kealakekua Bay", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "big-island-hawaii", + "ref": "scuba-diving-in-kealakekua-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_scuba-diving-in-kealakekua-bay.jpg" + }, + { + "name": "Road Trip Along the Hamakua Coast", + "description": "Embark on a scenic road trip along the Hamakua Coast, a stretch of dramatic coastline dotted with cascading waterfalls, lush rainforests, and charming towns. Stop at Akaka Falls State Park, explore the Hawaii Tropical Botanical Garden, and savor the local flavors at roadside fruit stands and cafes.", + "locationName": "Hamakua Coast", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-island-hawaii", + "ref": "road-trip-along-the-hamakua-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_road-trip-along-the-hamakua-coast.jpg" + }, + { + "name": "Visit the Pu'uhonua o Honaunau National Historical Park", + "description": "Step back in time at Pu'uhonua o Honaunau National Historical Park, a sacred place of refuge in ancient Hawaii. Explore the restored temples, learn about Hawaiian history and culture, and enjoy the serene beauty of this coastal park.", + "locationName": "Pu'uhonua o Honaunau National Historical Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-island-hawaii", + "ref": "visit-the-pu-uhonua-o-honaunau-national-historical-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-island-hawaii_visit-the-pu-uhonua-o-honaunau-national-historical-park.jpg" + }, + { + "name": "Hiking in Pfeiffer Big Sur State Park", + "description": "Explore the redwood groves and waterfalls of Pfeiffer Big Sur State Park on a network of scenic trails. Hike to Pfeiffer Falls, Valley View Trail, or the challenging Mount Manuel Trail for panoramic vistas.", + "locationName": "Pfeiffer Big Sur State Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-sur", + "ref": "hiking-in-pfeiffer-big-sur-state-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_hiking-in-pfeiffer-big-sur-state-park.jpg" + }, + { + "name": "Scenic Drive along Pacific Coast Highway", + "description": "Embark on an unforgettable road trip along the Pacific Coast Highway, stopping at iconic viewpoints like Bixby Creek Bridge, McWay Falls, and Pfeiffer Beach. Enjoy the dramatic cliffs, ocean views, and charming coastal towns.", + "locationName": "Pacific Coast Highway", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "big-sur", + "ref": "scenic-drive-along-pacific-coast-highway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_scenic-drive-along-pacific-coast-highway.jpg" + }, + { + "name": "Camping under the Redwoods", + "description": "Immerse yourself in nature by camping at one of the campgrounds in Big Sur. Pfeiffer Big Sur State Park, Julia Pfeiffer Burns State Park, and Plaskett Creek Campground offer stunning redwood forest settings and access to hiking trails.", + "locationName": "Pfeiffer Big Sur State Park, Julia Pfeiffer Burns State Park, Plaskett Creek Campground", + "duration": 12, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-sur", + "ref": "camping-under-the-redwoods", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_camping-under-the-redwoods.jpg" + }, + { + "name": "Whale Watching Tour", + "description": "Embark on a whale-watching tour from Monterey or Moss Landing to witness the majestic gray whales migrating along the Big Sur coast. Spot other marine life like dolphins, sea lions, and sea otters.", + "locationName": "Monterey or Moss Landing", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-sur", + "ref": "whale-watching-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_whale-watching-tour.jpg" + }, + { + "name": "Relaxation and Wellness Retreat", + "description": "Indulge in a rejuvenating experience at a wellness retreat in Big Sur. Enjoy yoga classes, spa treatments, meditation sessions, and breathtaking views of the natural surroundings.", + "locationName": "Esalen Institute, Post Ranch Inn", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "big-sur", + "ref": "relaxation-and-wellness-retreat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_relaxation-and-wellness-retreat.jpg" + }, + { + "name": "Explore Point Lobos State Natural Reserve", + "description": "Embark on a mesmerizing journey through Point Lobos State Natural Reserve, often referred to as the 'crown jewel' of California's state park system. Hike along scenic trails that wind through dramatic cliffs, hidden coves, and serene forests, offering breathtaking views of the Pacific Ocean. Discover diverse marine life at Whalers Cove and witness the playful antics of sea otters frolicking in the kelp forests.", + "locationName": "Point Lobos State Natural Reserve", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-sur", + "ref": "explore-point-lobos-state-natural-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_explore-point-lobos-state-natural-reserve.jpg" + }, + { + "name": "Indulge in a Romantic Getaway at Post Ranch Inn", + "description": "Escape to the luxurious embrace of Post Ranch Inn, a secluded cliffside retreat renowned for its breathtaking ocean views and unparalleled service. Indulge in a romantic getaway with your loved one, enjoying private clifftop accommodations, rejuvenating spa treatments, and gourmet dining experiences. Unwind by the infinity pool overlooking the vast Pacific or explore the surrounding natural wonders.", + "locationName": "Post Ranch Inn", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "big-sur", + "ref": "indulge-in-a-romantic-getaway-at-post-ranch-inn", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_indulge-in-a-romantic-getaway-at-post-ranch-inn.jpg" + }, + { + "name": "Kayaking Adventure at Pfeiffer Beach", + "description": "Embark on an unforgettable kayaking adventure at Pfeiffer Beach, renowned for its iconic rock formations and dramatic coastline. Paddle through turquoise waters, exploring hidden coves and sea caves while marveling at the majestic cliffs and the unique purple sand beach. Witness the power of the ocean as waves crash against the rocks, creating a truly awe-inspiring experience.", + "locationName": "Pfeiffer Beach", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-sur", + "ref": "kayaking-adventure-at-pfeiffer-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_kayaking-adventure-at-pfeiffer-beach.jpg" + }, + { + "name": "Art and Inspiration at the Henry Miller Memorial Library", + "description": "Delve into the world of renowned author Henry Miller at the Henry Miller Memorial Library. This unique cultural haven offers a glimpse into Miller's life and works, with a collection of his books, letters, and personal belongings. Attend literary events, art exhibitions, or simply relax in the tranquil garden setting, surrounded by the inspiring beauty of Big Sur.", + "locationName": "Henry Miller Memorial Library", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "big-sur", + "ref": "art-and-inspiration-at-the-henry-miller-memorial-library", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_art-and-inspiration-at-the-henry-miller-memorial-library.jpg" + }, + { + "name": "Horseback Riding through Redwoods", + "description": "Embark on an unforgettable horseback riding adventure through the majestic redwood forests of Big Sur. Several local ranches offer guided tours suitable for all skill levels, allowing you to immerse yourself in the serene beauty of these ancient giants.", + "locationName": "Glendeven Ranch or Molera Horseback Tours", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-sur", + "ref": "horseback-riding-through-redwoods", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_horseback-riding-through-redwoods.jpg" + }, + { + "name": "Stargazing at Pfeiffer Big Sur State Park", + "description": "Escape the city lights and experience the magic of stargazing in Big Sur. The clear night skies offer incredible views of constellations, planets, and even the Milky Way. Pfeiffer Big Sur State Park provides an ideal setting for this celestial wonder.", + "locationName": "Pfeiffer Big Sur State Park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "big-sur", + "ref": "stargazing-at-pfeiffer-big-sur-state-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_stargazing-at-pfeiffer-big-sur-state-park.jpg" + }, + { + "name": "Bixby Creek Bridge Photography", + "description": "Capture the iconic Bixby Creek Bridge, an architectural marvel and one of the most photographed bridges in California. Visit at sunrise or sunset for breathtaking golden hour shots, or explore different angles and perspectives to create your own unique composition.", + "locationName": "Bixby Creek Bridge", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "big-sur", + "ref": "bixby-creek-bridge-photography", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_bixby-creek-bridge-photography.jpg" + }, + { + "name": "Explore the Point Sur Lightstation", + "description": "Step back in time with a tour of the historic Point Sur Lightstation. This beautifully preserved lighthouse offers stunning coastal views and a glimpse into the lives of lighthouse keepers. Guided tours provide fascinating insights into the maritime history and technology of the area.", + "locationName": "Point Sur Lightstation", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-sur", + "ref": "explore-the-point-sur-lightstation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_explore-the-point-sur-lightstation.jpg" + }, + { + "name": "Wine Tasting at a Local Vineyard", + "description": "Indulge in the flavors of the Central Coast with a visit to a local vineyard. Several wineries in the Big Sur region offer tasting experiences, allowing you to sample a variety of wines while enjoying the picturesque vineyard landscapes. ", + "locationName": "Big Sur Vineyards or De Tierra Vineyards", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "big-sur", + "ref": "wine-tasting-at-a-local-vineyard", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_wine-tasting-at-a-local-vineyard.jpg" + }, + { + "name": "Surf's Up at Sand Dollar Beach", + "description": "Catch some waves and experience the thrill of surfing at Sand Dollar Beach, known for its consistent swells and stunning coastal views. Whether you're a seasoned surfer or a beginner eager to learn, local surf schools offer lessons and equipment rentals to get you started on your surfing adventure.", + "locationName": "Sand Dollar Beach", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "big-sur", + "ref": "surf-s-up-at-sand-dollar-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_surf-s-up-at-sand-dollar-beach.jpg" + }, + { + "name": "Explore the Underwater World with a Scuba Diving Excursion", + "description": "Dive into the depths of the Pacific Ocean and discover the vibrant marine life that thrives beneath the surface. Join a guided scuba diving excursion to explore kelp forests, encounter colorful fish, and witness the wonders of the underwater ecosystem.", + "locationName": "Monterey Bay", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "big-sur", + "ref": "explore-the-underwater-world-with-a-scuba-diving-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_explore-the-underwater-world-with-a-scuba-diving-excursion.jpg" + }, + { + "name": "Biking the Big Sur Coastline", + "description": "Embark on a scenic bike ride along the Pacific Coast Highway, pedaling past breathtaking ocean vistas, towering cliffs, and charming coastal towns. Rent a bike and explore at your own pace, or join a guided cycling tour for a more informative and social experience.", + "locationName": "Pacific Coast Highway", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "big-sur", + "ref": "biking-the-big-sur-coastline", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_biking-the-big-sur-coastline.jpg" + }, + { + "name": "Indulge in a Gourmet Food Tour", + "description": "Tantalize your taste buds with a culinary journey through Big Sur's renowned restaurants and local eateries. Embark on a guided food tour to sample fresh seafood, farm-to-table cuisine, and other regional delicacies, while learning about the area's culinary scene and cultural influences.", + "locationName": "Various locations in Big Sur", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "big-sur", + "ref": "indulge-in-a-gourmet-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_indulge-in-a-gourmet-food-tour.jpg" + }, + { + "name": "Soar Through the Skies on a Helicopter Tour", + "description": "Experience the awe-inspiring beauty of Big Sur from a whole new perspective with a thrilling helicopter tour. Soar above the rugged coastline, redwood forests, and cascading waterfalls, capturing breathtaking aerial views and creating unforgettable memories.", + "locationName": "Monterey Regional Airport", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "big-sur", + "ref": "soar-through-the-skies-on-a-helicopter-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/big-sur_soar-through-the-skies-on-a-helicopter-tour.jpg" + }, + { + "name": "Overwater Bungalow Luxury", + "description": "Indulge in the quintessential Bora Bora experience by staying in an overwater bungalow. Wake up to breathtaking views of the turquoise lagoon, step directly into the crystal-clear waters from your private deck, and enjoy unparalleled privacy and tranquility. Many bungalows feature glass floors for observing marine life below, and some offer private plunge pools or direct access to the lagoon for snorkeling or kayaking.", + "locationName": "Luxury resorts around Bora Bora", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "bora-bora", + "ref": "overwater-bungalow-luxury", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_overwater-bungalow-luxury.jpg" + }, + { + "name": "Snorkeling in Coral Gardens", + "description": "Explore the vibrant underwater world of Bora Bora's coral reefs. Numerous snorkeling spots around the island offer opportunities to encounter colorful fish, graceful manta rays, and even gentle sharks. Popular locations include the Coral Gardens, where shallow waters teem with marine life, and the Lagoonarium, a natural aquarium with a diverse array of species.", + "locationName": "Coral Gardens, Lagoonarium", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "bora-bora", + "ref": "snorkeling-in-coral-gardens", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_snorkeling-in-coral-gardens.jpg" + }, + { + "name": "Mount Otemanu Hike", + "description": "Embark on a challenging yet rewarding hike to the summit of Mount Otemanu, Bora Bora's iconic extinct volcano. The panoramic views from the top are simply breathtaking, offering a 360-degree perspective of the island, lagoon, and surrounding motus. This hike is best suited for experienced individuals due to its steep and rugged terrain.", + "locationName": "Mount Otemanu", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "bora-bora", + "ref": "mount-otemanu-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_mount-otemanu-hike.jpg" + }, + { + "name": "Sunset Cruise and Polynesian Dinner", + "description": "Set sail on a romantic sunset cruise around the lagoon, enjoying the vibrant colors painting the sky as the sun dips below the horizon. Many cruises include a delicious Polynesian dinner featuring local delicacies and traditional entertainment, such as fire dancers and live music. This experience offers a perfect blend of relaxation, stunning scenery, and cultural immersion.", + "locationName": "Bora Bora Lagoon", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "bora-bora", + "ref": "sunset-cruise-and-polynesian-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_sunset-cruise-and-polynesian-dinner.jpg" + }, + { + "name": "Island Jeep Safari", + "description": "Explore the interior of Bora Bora on an adventurous jeep safari. Traverse rugged terrain, visit historical sites like ancient temples and WWII remnants, and discover hidden viewpoints offering panoramic vistas. Learn about the island's flora, fauna, and Polynesian culture from knowledgeable guides as you venture off the beaten path.", + "locationName": "Bora Bora interior", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bora-bora", + "ref": "island-jeep-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_island-jeep-safari.jpg" + }, + { + "name": "Lagoonarium de Bora Bora", + "description": "Embark on an unforgettable underwater adventure at the Lagoonarium de Bora Bora, a natural aquarium nestled within the lagoon. This eco-friendly sanctuary allows you to swim alongside stingrays, blacktip reef sharks, turtles, and a kaleidoscope of tropical fish in their natural habitat. Knowledgeable guides provide insights into the marine ecosystem, making it a fun and educational experience for all ages.", + "locationName": "Lagoonarium de Bora Bora", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "bora-bora", + "ref": "lagoonarium-de-bora-bora", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_lagoonarium-de-bora-bora.jpg" + }, + { + "name": "Bora Bora Turtle Center", + "description": "Visit the Bora Bora Turtle Center at Le Méridien Bora Bora resort and witness the incredible conservation efforts dedicated to protecting sea turtles. Observe these gentle creatures up close, learn about their life cycle, and even participate in feeding them. This heartwarming experience is perfect for families and those passionate about marine life.", + "locationName": "Le Méridien Bora Bora", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bora-bora", + "ref": "bora-bora-turtle-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_bora-bora-turtle-center.jpg" + }, + { + "name": "Romantic Private Motu Picnic", + "description": "Indulge in a secluded and romantic experience with a private motu picnic. Escape to your own tiny islet surrounded by turquoise waters and enjoy a gourmet picnic basket filled with delectable treats. Bask in the sun, swim in the crystal-clear lagoon, and create unforgettable memories with your loved one.", + "locationName": "Various motu (islets) around Bora Bora", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "bora-bora", + "ref": "romantic-private-motu-picnic", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_romantic-private-motu-picnic.jpg" + }, + { + "name": "Vaitape Village Exploration", + "description": "Immerse yourself in the local culture with a visit to Vaitape, Bora Bora's main village. Explore the charming streets lined with shops selling Polynesian crafts, pearls, and souvenirs. Visit the local church, sample delicious Polynesian cuisine at family-run restaurants, and interact with friendly locals to get a glimpse into their way of life.", + "locationName": "Vaitape Village", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bora-bora", + "ref": "vaitape-village-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_vaitape-village-exploration.jpg" + }, + { + "name": "Sunset Paddleboarding or Kayaking", + "description": "Experience the magic of a Bora Bora sunset from a unique perspective with a paddleboarding or kayaking excursion. Glide across the calm lagoon waters as the sky transforms into a canvas of vibrant colors. Witness the silhouette of Mount Otemanu against the setting sun, creating a truly breathtaking and unforgettable moment.", + "locationName": "Various locations around the lagoon", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "bora-bora", + "ref": "sunset-paddleboarding-or-kayaking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_sunset-paddleboarding-or-kayaking.jpg" + }, + { + "name": "Parasailing Over the Lagoon", + "description": "Soar high above the crystal-clear waters of Bora Bora's lagoon and take in breathtaking panoramic views of the island and surrounding motus. Feel the wind in your hair as you glide through the air, enjoying a unique perspective of this tropical paradise.", + "locationName": "Bora Bora Lagoon", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "bora-bora", + "ref": "parasailing-over-the-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_parasailing-over-the-lagoon.jpg" + }, + { + "name": "Jet Ski Adventure to Hidden Coves", + "description": "Embark on an exhilarating jet ski tour around the island, discovering secluded coves and hidden beaches inaccessible by foot. Zip through the turquoise waters, feeling the spray of the ocean and the thrill of adventure.", + "locationName": "Various locations around Bora Bora", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "bora-bora", + "ref": "jet-ski-adventure-to-hidden-coves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_jet-ski-adventure-to-hidden-coves.jpg" + }, + { + "name": "Deep-Sea Fishing Excursion", + "description": "Test your angling skills on a deep-sea fishing adventure, departing from Bora Bora's shores. Cast your line and try your luck at catching marlin, tuna, mahi-mahi, and other prized fish. Enjoy the thrill of the chase and the satisfaction of reeling in your catch.", + "locationName": "Pacific Ocean surrounding Bora Bora", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "bora-bora", + "ref": "deep-sea-fishing-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_deep-sea-fishing-excursion.jpg" + }, + { + "name": " Polynesian Dance Performance and Fire Show", + "description": "Immerse yourself in the vibrant Polynesian culture with a captivating dance performance and fire show. Witness the graceful movements of the dancers, the rhythmic beats of the drums, and the mesmerizing fire displays, experiencing the true essence of the islands.", + "locationName": "Various resorts and cultural venues", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "bora-bora", + "ref": "-polynesian-dance-performance-and-fire-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_-polynesian-dance-performance-and-fire-show.jpg" + }, + { + "name": "Private Island Hopping by Boat", + "description": "Charter a private boat and embark on a personalized island-hopping adventure around the Bora Bora lagoon. Explore secluded motus, snorkel in pristine coral gardens, and enjoy a picnic lunch on a deserted beach, relishing the freedom and exclusivity of your own island escape.", + "locationName": "Bora Bora Lagoon and surrounding motus", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "bora-bora", + "ref": "private-island-hopping-by-boat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_private-island-hopping-by-boat.jpg" + }, + { + "name": "Bora Bora Lagoonarium Eco-Tour", + "description": "Embark on a unique and educational journey to the Lagoonarium, a natural aquarium located within the Bora Bora lagoon. Snorkel alongside an array of marine life, including stingrays, sharks, turtles, and colorful fish. Learn about the delicate ecosystem of the lagoon and the importance of conservation efforts from knowledgeable guides.", + "locationName": "Lagoonarium de Bora Bora", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "bora-bora", + "ref": "bora-bora-lagoonarium-eco-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_bora-bora-lagoonarium-eco-tour.jpg" + }, + { + "name": "Matira Beach Relaxation and Watersports", + "description": "Indulge in the ultimate beach day at Matira Beach, renowned for its powdery white sand and crystal-clear waters. Unwind under the shade of palm trees, swim in the refreshing lagoon, or try exciting watersports such as stand-up paddleboarding, kayaking, or windsurfing. Beachside cafes and restaurants offer delicious refreshments and local cuisine.", + "locationName": "Matira Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bora-bora", + "ref": "matira-beach-relaxation-and-watersports", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_matira-beach-relaxation-and-watersports.jpg" + }, + { + "name": "Mount Pahia Hiking Adventure", + "description": "Challenge yourself with a hike to the summit of Mount Pahia, the highest point on Bora Bora. Enjoy breathtaking panoramic views of the lagoon, motus, and surrounding islands. This moderately strenuous hike takes you through lush rainforests and rewards you with unforgettable vistas at the top.", + "locationName": "Mount Pahia", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "bora-bora", + "ref": "mount-pahia-hiking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_mount-pahia-hiking-adventure.jpg" + }, + { + "name": "Polynesian Cooking Class and Cultural Immersion", + "description": "Delve into the heart of Polynesian culture with a hands-on cooking class. Learn to prepare traditional dishes using fresh local ingredients, such as poisson cru (marinated fish), taro root, and breadfruit. Discover the secrets of Polynesian cuisine and enjoy a delicious feast of your own creations.", + "locationName": "Local village or resort", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "bora-bora", + "ref": "polynesian-cooking-class-and-cultural-immersion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_polynesian-cooking-class-and-cultural-immersion.jpg" + }, + { + "name": "Stargazing on a Secluded Motu", + "description": "Escape the city lights and embark on a magical stargazing experience on a private motu. With minimal light pollution, the night sky above Bora Bora comes alive with countless stars and constellations. Learn about Polynesian navigation techniques and celestial legends from a local guide.", + "locationName": "Private motu", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "bora-bora", + "ref": "stargazing-on-a-secluded-motu", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bora-bora_stargazing-on-a-secluded-motu.jpg" + }, + { + "name": "Mokoro Safari Excursion", + "description": "Embark on a tranquil mokoro (traditional dugout canoe) safari through the serene waterways of the Okavango Delta. Glide past lush vegetation, encountering elephants, hippos, crocodiles, and a myriad of bird species in their natural habitat. Your experienced guide will share insights into the delta's ecosystem and the fascinating wildlife that call it home.", + "locationName": "Okavango Delta waterways", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "botswana", + "ref": "mokoro-safari-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_mokoro-safari-excursion.jpg" + }, + { + "name": "Wildlife Safari Game Drive", + "description": "Venture into the heart of the Okavango Delta on an exhilarating 4x4 game drive. Led by expert guides, you'll track lions, leopards, elephants, buffalo, and other iconic African animals. Witness the drama of the wild unfold before your eyes, capturing incredible photographs and creating unforgettable memories.", + "locationName": "Moremi Game Reserve or private concessions", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "botswana", + "ref": "wildlife-safari-game-drive", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_wildlife-safari-game-drive.jpg" + }, + { + "name": "Scenic Helicopter Flight", + "description": "Take to the skies on a breathtaking helicopter flight over the Okavango Delta. Marvel at the vastness of the delta, its intricate network of waterways, and the abundance of wildlife from a unique aerial perspective. This unforgettable experience offers a true appreciation for the scale and beauty of this natural wonder.", + "locationName": "Various departure points within the delta", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "botswana", + "ref": "scenic-helicopter-flight", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_scenic-helicopter-flight.jpg" + }, + { + "name": "Bushwalk with San Bushmen", + "description": "Immerse yourself in the ancient culture of the San Bushmen, the indigenous people of the Kalahari Desert. Join them on a guided bushwalk, learning about their traditional hunting techniques, survival skills, and deep connection with the natural world. This cultural experience offers a unique perspective on the history and heritage of the region.", + "locationName": "Specific cultural villages or camps", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "botswana", + "ref": "bushwalk-with-san-bushmen", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_bushwalk-with-san-bushmen.jpg" + }, + { + "name": "Stargazing Experience", + "description": "Escape the city lights and immerse yourself in the dazzling night sky of the Okavango Delta. With minimal light pollution, the stars shine with exceptional brilliance. Join a guided stargazing session to learn about constellations, planets, and the wonders of the universe.", + "locationName": "Open areas within lodges or camps", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "botswana", + "ref": "stargazing-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_stargazing-experience.jpg" + }, + { + "name": "Horseback Safari", + "description": "Embark on a unique and exhilarating horseback safari, traversing the diverse landscapes of the delta. Experienced guides will lead you through floodplains, grasslands, and woodlands, allowing you to get up close to wildlife like giraffes, zebras, and antelopes. This eco-friendly activity offers a different perspective on the delta's beauty and allows you to connect with nature in a special way.", + "locationName": "Okavango Delta Horse Safaris", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "botswana", + "ref": "horseback-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_horseback-safari.jpg" + }, + { + "name": "Bird Watching Excursion", + "description": "The Okavango Delta is a birdwatcher's paradise, home to over 400 species of birds. Join a guided bird watching excursion led by expert ornithologists who will help you spot and identify a variety of feathered wonders, from the majestic African fish eagle to the colorful lilac-breasted roller. Learn about their unique behaviors, habitats, and the importance of bird conservation in the delta.", + "locationName": "Okavango Panhandle", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "botswana", + "ref": "bird-watching-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_bird-watching-excursion.jpg" + }, + { + "name": "Fishing Expedition", + "description": "Cast your line and experience the thrill of fishing in the pristine waters of the Okavango Delta. Target species like tigerfish, bream, and catfish, known for their fighting spirit and delicious taste. Whether you're a seasoned angler or a novice, local guides will provide the necessary equipment and expertise to ensure a memorable fishing adventure.", + "locationName": "Okavango River", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "botswana", + "ref": "fishing-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_fishing-expedition.jpg" + }, + { + "name": "Cultural Village Visit", + "description": "Immerse yourself in the rich cultural heritage of the local communities living in the Okavango Delta region. Visit a traditional village and interact with the friendly people, learning about their customs, traditions, and way of life. Enjoy traditional music and dance performances, sample local cuisine, and gain a deeper appreciation for the cultural diversity of Botswana.", + "locationName": "Local Villages", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "botswana", + "ref": "cultural-village-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_cultural-village-visit.jpg" + }, + { + "name": "Sunset Cruise with Traditional Dinner", + "description": "Experience the magic of the Okavango Delta at sunset with a leisurely cruise along its serene waterways. As the sun dips below the horizon, painting the sky with vibrant hues, savor a delicious traditional dinner prepared with local ingredients. Enjoy the tranquil atmosphere, observe nocturnal wildlife, and create unforgettable memories under the African night sky.", + "locationName": "Okavango Delta waterways", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "botswana", + "ref": "sunset-cruise-with-traditional-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_sunset-cruise-with-traditional-dinner.jpg" + }, + { + "name": "Quad Biking Adventure", + "description": "Embark on an adrenaline-pumping quad biking adventure through the rugged terrains of the Okavango Delta. Traverse sand dunes, navigate through bush trails, and experience the thrill of off-road exploration while taking in the breathtaking scenery.", + "locationName": "Okavango Delta", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "botswana", + "ref": "quad-biking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_quad-biking-adventure.jpg" + }, + { + "name": "Hot Air Balloon Safari", + "description": "Soar above the Okavango Delta in a hot air balloon and witness the majesty of the landscape from a unique perspective. Drift silently over the floodplains, savannas, and waterways, observing wildlife and enjoying panoramic views as the sun rises or sets.", + "locationName": "Okavango Delta", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "botswana", + "ref": "hot-air-balloon-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_hot-air-balloon-safari.jpg" + }, + { + "name": "Photographic Safari", + "description": "Join a specialized photographic safari designed for capturing the incredible wildlife and landscapes of the Okavango Delta. Learn from experienced guides and photographers, receiving expert tips and guidance on capturing stunning images of elephants, lions, birds, and more.", + "locationName": "Okavango Delta", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "botswana", + "ref": "photographic-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_photographic-safari.jpg" + }, + { + "name": "Scenic Helicopter Flight", + "description": "Take to the skies on a scenic helicopter flight over the Okavango Delta, offering unparalleled aerial views of the intricate waterways, islands, and wildlife. Marvel at the vastness of the delta and capture breathtaking photographs from above.", + "locationName": "Okavango Delta", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "botswana", + "ref": "scenic-helicopter-flight", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_scenic-helicopter-flight.jpg" + }, + { + "name": "Traditional Makoro Excursion", + "description": "Glide through the serene waterways of the Okavango Delta in a traditional makoro (dugout canoe) and experience the tranquility of the delta up close. Observe the abundant birdlife, encounter hippos and crocodiles, and learn about the unique ecosystem from local guides.", + "locationName": "Okavango Delta", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "botswana", + "ref": "traditional-makoro-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_traditional-makoro-excursion.jpg" + }, + { + "name": "Elephant Interaction Experience", + "description": "Get up close and personal with orphaned elephants at a sanctuary dedicated to their rehabilitation and conservation. Learn about their behavior, biology, and the challenges they face in the wild. You might even have the opportunity to feed and interact with these gentle giants, creating unforgettable memories.", + "locationName": "Elephant sanctuary within the Okavango Delta", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "botswana", + "ref": "elephant-interaction-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_elephant-interaction-experience.jpg" + }, + { + "name": "Sleep-Out Under the Stars", + "description": "Embark on an overnight adventure to a secluded platform nestled deep within the delta. Enjoy a delicious bush dinner cooked over an open fire, share stories under the starry sky, and drift off to sleep surrounded by the sounds of the African wilderness. Wake up to breathtaking sunrise views and the chance to spot wildlife as they begin their day.", + "locationName": "Remote location within the Okavango Delta", + "duration": 24, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "botswana", + "ref": "sleep-out-under-the-stars", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_sleep-out-under-the-stars.jpg" + }, + { + "name": "Guided Nature Walk", + "description": "Join a knowledgeable local guide on a walking safari through the diverse ecosystems of the delta. Discover hidden trails, learn about medicinal plants and their uses, track animal footprints, and gain a deeper understanding of the intricate web of life that exists within this unique environment.", + "locationName": "Various locations within the Okavango Delta", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "botswana", + "ref": "guided-nature-walk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_guided-nature-walk.jpg" + }, + { + "name": "Traditional Fishing with Locals", + "description": "Immerse yourself in the local culture by joining villagers on a traditional fishing trip. Learn ancient techniques passed down through generations, using hand-crafted equipment and natural bait. Experience the thrill of the catch and gain insight into the sustainable fishing practices that have sustained the communities of the delta for centuries.", + "locationName": "Local village near the Okavango Delta", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "botswana", + "ref": "traditional-fishing-with-locals", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_traditional-fishing-with-locals.jpg" + }, + { + "name": "Bushcraft and Survival Skills Workshop", + "description": "Develop essential wilderness skills with a hands-on bushcraft and survival workshop. Learn how to build a fire, identify edible plants, navigate using natural landmarks, and construct basic shelters. This immersive experience will connect you with the natural world and equip you with valuable knowledge for future adventures.", + "locationName": "Wilderness area within the Okavango Delta", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "botswana", + "ref": "bushcraft-and-survival-skills-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/botswana_bushcraft-and-survival-skills-workshop.jpg" + }, + { + "name": "Canal Boat Tour", + "description": "Embark on a serene boat tour through Bruges' enchanting canals, often referred to as the 'Venice of the North.' Glide under picturesque bridges, past charming medieval houses, and soak in the city's unique atmosphere from a different perspective. Learn about Bruges' rich history and architecture from your guide as you admire the beauty of this UNESCO World Heritage Site.", + "locationName": "Bruges canals", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bruges", + "ref": "canal-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_canal-boat-tour.jpg" + }, + { + "name": "Markt Square Exploration", + "description": "Step into the heart of Bruges at the Markt, the historic main square. Marvel at the colorful guildhalls, the imposing Belfry tower, and the Provincial Court. Enjoy the lively atmosphere, browse the shops for souvenirs, or relax at a cafe and soak up the vibrant ambiance. Don't miss the chance to climb the Belfry for panoramic views of the city.", + "locationName": "Markt Square", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "bruges", + "ref": "markt-square-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_markt-square-exploration.jpg" + }, + { + "name": "Chocolate Tasting and Workshop", + "description": "Indulge your sweet tooth with a delightful chocolate tasting experience. Bruges is renowned for its exquisite Belgian chocolate, and numerous shops offer tastings and workshops. Learn about the history of chocolate making, discover the different types of chocolate, and even try your hand at creating your own delicious treats.", + "locationName": "Various chocolate shops", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bruges", + "ref": "chocolate-tasting-and-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_chocolate-tasting-and-workshop.jpg" + }, + { + "name": "Groeningemuseum Visit", + "description": "Immerse yourself in the world of Flemish art at the Groeningemuseum. Explore a rich collection of paintings by renowned artists, including Jan van Eyck, Hans Memling, and Hieronymus Bosch. Admire masterpieces from the Flemish Primitives and learn about the evolution of art in the region. The museum offers a fascinating journey through Bruges' artistic heritage.", + "locationName": "Groeningemuseum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "bruges", + "ref": "groeningemuseum-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_groeningemuseum-visit.jpg" + }, + { + "name": "Horse-Drawn Carriage Ride", + "description": "Experience Bruges in a truly romantic way with a horse-drawn carriage ride. Snuggle up with your loved one as you travel through the cobbled streets and admire the city's charming architecture. This leisurely ride offers a unique perspective of Bruges and allows you to relax and enjoy the magical atmosphere.", + "locationName": "Markt Square and surrounding streets", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bruges", + "ref": "horse-drawn-carriage-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_horse-drawn-carriage-ride.jpg" + }, + { + "name": "Belgium Pierogi & Beer Pairing Experience", + "description": "Embark on a delectable journey for your taste buds with a unique pierogi and beer pairing experience. Sample a variety of traditional and modern pierogi, each expertly matched with a complementary Belgian beer, highlighting the diverse flavors of the region. Discover the art of pairing, learn about the brewing process, and enjoy a convivial atmosphere perfect for socializing and indulging in culinary delights.", + "locationName": "Local brewery or restaurant", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "bruges", + "ref": "belgium-pierogi-beer-pairing-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_belgium-pierogi-beer-pairing-experience.jpg" + }, + { + "name": "Flemish Gastronomy Cooking Class", + "description": "Immerse yourself in the rich culinary traditions of Flanders with a hands-on cooking class. Learn to prepare classic Flemish dishes such as waterzooi, carbonnade flamande, or stoofvlees, under the guidance of a local chef. Discover the secrets of Flemish cuisine, from selecting fresh ingredients to mastering traditional cooking techniques. Enjoy the fruits of your labor with a delicious meal paired with local beers or wines.", + "locationName": "Cooking school or local home", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "bruges", + "ref": "flemish-gastronomy-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_flemish-gastronomy-cooking-class.jpg" + }, + { + "name": "Minnewater Lake and Lover's Bridge Stroll", + "description": "Escape the bustling city center and enjoy a romantic stroll around the serene Minnewater Lake, also known as the 'Lake of Love'. Amble across the charming Lover's Bridge, shrouded in local legends and folklore, and soak in the picturesque scenery of the surrounding park. This tranquil setting is perfect for a leisurely walk, a picnic by the water, or simply enjoying a quiet moment with a loved one.", + "locationName": "Minnewater Park", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "bruges", + "ref": "minnewater-lake-and-lover-s-bridge-stroll", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_minnewater-lake-and-lover-s-bridge-stroll.jpg" + }, + { + "name": "Lacemaking Demonstration and Workshop", + "description": "Delve into the intricate world of Bruges' renowned lacemaking tradition with a visit to a local lace workshop. Witness skilled artisans demonstrating the delicate art of bobbin lacemaking, a centuries-old craft passed down through generations. Learn about the history and significance of lace in Bruges, and even try your hand at creating your own lace masterpiece with a hands-on workshop.", + "locationName": "Kantcentrum (Lace Centre)", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "bruges", + "ref": "lacemaking-demonstration-and-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_lacemaking-demonstration-and-workshop.jpg" + }, + { + "name": "Bike Tour through the Countryside", + "description": "Venture beyond the city walls and explore the idyllic Flemish countryside on a leisurely bike tour. Pedal along scenic paths, passing picturesque villages, verdant fields, and charming windmills. Discover hidden gems off the beaten path, breathe in the fresh air, and experience the tranquility of rural Flanders. Bike tours can be tailored to different fitness levels and interests, offering a unique perspective on the region's natural beauty.", + "locationName": "Bruges and surrounding countryside", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bruges", + "ref": "bike-tour-through-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_bike-tour-through-the-countryside.jpg" + }, + { + "name": "Climb the Belfry of Bruges for Panoramic Views", + "description": "Ascend the 366 steps of the iconic Belfry of Bruges, a medieval bell tower that offers breathtaking panoramic views of the city and surrounding countryside. Learn about the history and significance of the bell tower while enjoying a unique perspective of Bruges' charming canals, cobblestone streets, and historic buildings.", + "locationName": "Markt Square", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bruges", + "ref": "climb-the-belfry-of-bruges-for-panoramic-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_climb-the-belfry-of-bruges-for-panoramic-views.jpg" + }, + { + "name": "Indulge in a Belgian Beer Tasting Experience", + "description": "Immerse yourself in Belgium's renowned beer culture with a guided tasting experience at a local brewery or pub. Sample a variety of traditional Belgian beers, from Trappist ales to lambics, and learn about the brewing process, history, and unique flavors of each style. This activity is perfect for beer enthusiasts and those looking to discover the local flavors.", + "locationName": "Various breweries and pubs", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "bruges", + "ref": "indulge-in-a-belgian-beer-tasting-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_indulge-in-a-belgian-beer-tasting-experience.jpg" + }, + { + "name": "Explore the Historic Beguinage", + "description": "Step into the serene and historic Beguinage, a UNESCO World Heritage site that was once home to a community of religious women. Wander through the peaceful courtyard, admire the white-washed houses, and learn about the unique history and traditions of the Beguines. This tranquil oasis offers a respite from the bustling city and a glimpse into Bruges' past.", + "locationName": "Beguinage", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "bruges", + "ref": "explore-the-historic-beguinage", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_explore-the-historic-beguinage.jpg" + }, + { + "name": "Enjoy a Romantic Evening Canal Cruise", + "description": "Experience the enchanting beauty of Bruges at dusk with a romantic evening canal cruise. Glide along the illuminated waterways, admiring the city's historic buildings and bridges bathed in soft light. Enjoy the peaceful ambiance, learn about the city's history from your guide, and create lasting memories with your loved one.", + "locationName": "Bruges canals", + "duration": 1.5, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "bruges", + "ref": "enjoy-a-romantic-evening-canal-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_enjoy-a-romantic-evening-canal-cruise.jpg" + }, + { + "name": "Day Trip to Ghent", + "description": "Embark on a delightful day trip to the nearby city of Ghent, another historical gem in Flanders. Explore the Gravensteen castle, a medieval fortress dating back to the 12th century, and admire the Ghent Altarpiece in St. Bavo's Cathedral. Wander along the Graslei, a picturesque waterfront lined with guildhalls, and soak up the vibrant atmosphere of this charming city.", + "locationName": "Ghent", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "bruges", + "ref": "day-trip-to-ghent", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_day-trip-to-ghent.jpg" + }, + { + "name": "Concert at the Concertgebouw", + "description": "Experience the magic of live music at the Concertgebouw, a renowned concert hall known for its exceptional acoustics and diverse program. Choose from classical concerts, jazz performances, world music, or contemporary shows, and immerse yourself in the cultural richness of Bruges.", + "locationName": "Concertgebouw", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "bruges", + "ref": "concert-at-the-concertgebouw", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_concert-at-the-concertgebouw.jpg" + }, + { + "name": "Frites and Beer Pairing", + "description": "Indulge in a quintessential Belgian experience with a frites and beer pairing. Sample a variety of crispy, golden fries from a local frituur, accompanied by a selection of craft beers from renowned Belgian breweries. Discover the perfect combinations of flavors and textures, and savor the authentic taste of Bruges.", + "locationName": "Local frituur and brewery", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "bruges", + "ref": "frites-and-beer-pairing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_frites-and-beer-pairing.jpg" + }, + { + "name": "Explore the Frietmuseum", + "description": "Delve into the fascinating history of the humble potato fry at the Frietmuseum, the world's only museum dedicated to fries. Learn about the origins of this beloved dish, its cultural significance, and the various ways it is prepared around the world. Enjoy interactive exhibits and, of course, a tasting of delicious Belgian fries.", + "locationName": "Frietmuseum", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "bruges", + "ref": "explore-the-frietmuseum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_explore-the-frietmuseum.jpg" + }, + { + "name": "Hot Air Balloon Ride over Bruges", + "description": "Soar above the enchanting city of Bruges in a hot air balloon and witness breathtaking panoramic views of its medieval rooftops, winding canals, and picturesque countryside. Enjoy a unique perspective of this historic city as you drift peacefully through the sky, creating an unforgettable memory.", + "locationName": "Bruges Countryside", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "bruges", + "ref": "hot-air-balloon-ride-over-bruges", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/bruges_hot-air-balloon-ride-over-bruges.jpg" + }, + { + "name": "Immerse Yourself in the Water Village of Kampong Ayer", + "description": "Embark on a boat tour through the fascinating water village of Kampong Ayer, often called the \"Venice of the East.\" Explore the intricate network of stilt houses, witness the daily life of the locals, and visit museums and shops showcasing traditional crafts.", + "locationName": "Kampong Ayer", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "brunei", + "ref": "immerse-yourself-in-the-water-village-of-kampong-ayer", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_immerse-yourself-in-the-water-village-of-kampong-ayer.jpg" + }, + { + "name": "Marvel at the Majesty of Sultan Omar Ali Saifuddin Mosque", + "description": "Visit the iconic Sultan Omar Ali Saifuddin Mosque, an architectural masterpiece with a golden dome and intricate mosaics. Explore the serene interior, admire the stunning views of the Brunei River, and learn about the significance of this religious landmark.", + "locationName": "Sultan Omar Ali Saifuddin Mosque", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "brunei", + "ref": "marvel-at-the-majesty-of-sultan-omar-ali-saifuddin-mosque", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_marvel-at-the-majesty-of-sultan-omar-ali-saifuddin-mosque.jpg" + }, + { + "name": "Venture into the Ulu Temburong National Park", + "description": "Embark on a thrilling adventure into the heart of the Borneo rainforest at Ulu Temburong National Park. Hike through lush trails, discover hidden waterfalls, climb the canopy walkway for breathtaking views, and encounter diverse wildlife.", + "locationName": "Ulu Temburong National Park", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "brunei", + "ref": "venture-into-the-ulu-temburong-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_venture-into-the-ulu-temburong-national-park.jpg" + }, + { + "name": "Delve into Brunei's Royal Heritage at the Royal Regalia Museum", + "description": "Explore the Royal Regalia Museum, home to a vast collection of treasures and artifacts showcasing the history and grandeur of the Brunei Sultanate. Admire the Sultan's coronation chariot, jeweled weaponry, and exquisite gifts from dignitaries around the world.", + "locationName": "Royal Regalia Museum", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "brunei", + "ref": "delve-into-brunei-s-royal-heritage-at-the-royal-regalia-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_delve-into-brunei-s-royal-heritage-at-the-royal-regalia-museum.jpg" + }, + { + "name": "Experience Tranquility at the Istana Nurul Iman", + "description": "While entry inside is limited to special occasions, admiring the Istana Nurul Iman from the outside is an experience in itself. Capture the grandeur of the world's largest residential palace, a symbol of Brunei's royal heritage and architectural splendor.", + "locationName": "Istana Nurul Iman", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "brunei", + "ref": "experience-tranquility-at-the-istana-nurul-iman", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_experience-tranquility-at-the-istana-nurul-iman.jpg" + }, + { + "name": "Proboscis Monkey River Safari", + "description": "Embark on a captivating river cruise through the mangrove forests of Brunei, keeping an eye out for the iconic proboscis monkeys with their distinctive noses. Witness these fascinating primates in their natural habitat and observe other wildlife, like crocodiles and various bird species.", + "locationName": "Brunei River or Temburong River", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "brunei", + "ref": "proboscis-monkey-river-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_proboscis-monkey-river-safari.jpg" + }, + { + "name": "Gadong Night Market", + "description": "Experience the vibrant atmosphere of the Gadong Night Market, a bustling hub of local life. Browse through an array of stalls offering delicious street food, fresh produce, clothing, souvenirs, and more. This is a great place to immerse yourself in the local culture and indulge in authentic Bruneian flavors.", + "locationName": "Gadong", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "brunei", + "ref": "gadong-night-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_gadong-night-market.jpg" + }, + { + "name": "Tasek Lama Recreational Park", + "description": "Escape the city bustle and enjoy the tranquility of Tasek Lama Recreational Park. Hike or bike through the lush rainforest trails, discover hidden waterfalls, and visit the serene Tasek Lama Lake. The park also features picnic areas, playgrounds, and a swimming pool, making it perfect for a family outing.", + "locationName": "Tasek Lama Recreational Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "brunei", + "ref": "tasek-lama-recreational-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_tasek-lama-recreational-park.jpg" + }, + { + "name": "Jerudong Park Playground", + "description": "Spend a fun-filled day at Jerudong Park Playground, Brunei's largest and most popular amusement park. Enjoy thrilling rides, entertaining shows, and various attractions suitable for all ages. The park also features beautifully landscaped gardens and a musical fountain, providing a delightful experience for the whole family.", + "locationName": "Jerudong Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "brunei", + "ref": "jerudong-park-playground", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_jerudong-park-playground.jpg" + }, + { + "name": "Ulu Temburong National Park Canopy Walk", + "description": "Embark on an exhilarating canopy walk adventure in the heart of Ulu Temburong National Park. Ascend to the rainforest canopy via a series of suspended walkways and bridges, offering breathtaking panoramic views of the pristine rainforest and its diverse flora and fauna.", + "locationName": "Ulu Temburong National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "brunei", + "ref": "ulu-temburong-national-park-canopy-walk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_ulu-temburong-national-park-canopy-walk.jpg" + }, + { + "name": "Explore the Seria Energy Lab", + "description": "Dive into the world of energy at the Seria Energy Lab, an interactive museum showcasing Brunei's oil and gas industry. Learn about the science behind energy production, the history of Brunei's oil reserves, and the future of sustainable energy solutions.", + "locationName": "Seria", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "brunei", + "ref": "explore-the-seria-energy-lab", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_explore-the-seria-energy-lab.jpg" + }, + { + "name": "Go Birdwatching in the Mangrove Forests", + "description": "Embark on a guided tour through the lush mangrove forests of Brunei, a haven for diverse bird species. Spot colorful kingfishers, majestic eagles, and playful monkeys while learning about the importance of these ecosystems.", + "locationName": "Mangrove forests around Brunei Bay", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "brunei", + "ref": "go-birdwatching-in-the-mangrove-forests", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_go-birdwatching-in-the-mangrove-forests.jpg" + }, + { + "name": "Discover Local Crafts at the Arts and Handicrafts Training Center", + "description": "Immerse yourself in Brunei's artistic heritage at the Arts and Handicrafts Training Center. Witness skilled artisans crafting traditional textiles, silverware, and wood carvings. You can even purchase unique souvenirs to support local craftsmanship.", + "locationName": "Bandar Seri Begawan", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "brunei", + "ref": "discover-local-crafts-at-the-arts-and-handicrafts-training-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_discover-local-crafts-at-the-arts-and-handicrafts-training-center.jpg" + }, + { + "name": "Relax on the Pristine Beaches of Tutong", + "description": "Escape the bustle and unwind on the tranquil beaches of Tutong. Sunbathe on the golden sands, take a refreshing dip in the turquoise waters, or simply enjoy a peaceful stroll along the coastline. The serene atmosphere is perfect for relaxation and rejuvenation.", + "locationName": "Tutong District", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "brunei", + "ref": "relax-on-the-pristine-beaches-of-tutong", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_relax-on-the-pristine-beaches-of-tutong.jpg" + }, + { + "name": "Indulge in a Culinary Journey", + "description": "Embark on a gastronomic adventure through Brunei's diverse culinary scene. Sample local delicacies like Ambuyat, Nasi Katok, and Satay at bustling markets or charming restaurants. Don't miss the chance to try unique flavors influenced by Malay, Chinese, and Indian cuisines.", + "locationName": "Various locations throughout Brunei", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "brunei", + "ref": "indulge-in-a-culinary-journey", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_indulge-in-a-culinary-journey.jpg" + }, + { + "name": "Go spelunking in the Gomantong Caves", + "description": "Embark on an adventurous journey into the depths of the Gomantong Caves, a renowned cave system famed for its swiftlet nests and diverse bat population. Witness the unique ecosystem within the caves and observe the fascinating interactions between these creatures and their environment. Opt for a guided tour to navigate the caves safely and learn about their geological significance and cultural importance.", + "locationName": "Gomantong Caves", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "brunei", + "ref": "go-spelunking-in-the-gomantong-caves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_go-spelunking-in-the-gomantong-caves.jpg" + }, + { + "name": "Dive into the Shipwrecks of Brunei Bay", + "description": "Explore the underwater world of Brunei Bay, where several shipwrecks from World War II lie beneath the surface. Discover the historical significance of these wrecks as you encounter diverse marine life and coral reefs. Whether you're a seasoned diver or a beginner, numerous dive operators offer guided excursions suitable for various skill levels.", + "locationName": "Brunei Bay", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "brunei", + "ref": "dive-into-the-shipwrecks-of-brunei-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_dive-into-the-shipwrecks-of-brunei-bay.jpg" + }, + { + "name": "Shop for Unique Souvenirs at Tamu Kianggeh", + "description": "Immerse yourself in the vibrant atmosphere of Tamu Kianggeh, a bustling open-air market along the Brunei River. Discover a treasure trove of local handicrafts, fresh produce, and unique souvenirs. Engage with friendly vendors, sample traditional delicacies, and experience the authentic Bruneian way of life.", + "locationName": "Tamu Kianggeh", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "brunei", + "ref": "shop-for-unique-souvenirs-at-tamu-kianggeh", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_shop-for-unique-souvenirs-at-tamu-kianggeh.jpg" + }, + { + "name": "Take a Boat Trip to Pulau Selirong", + "description": "Escape to the tranquil island of Pulau Selirong, located off the coast of Brunei. Enjoy a scenic boat ride through the Brunei Bay, surrounded by lush mangroves and sparkling waters. Explore the island's pristine beaches, discover hidden coves, and relax amidst the serene natural beauty.", + "locationName": "Pulau Selirong", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "brunei", + "ref": "take-a-boat-trip-to-pulau-selirong", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_take-a-boat-trip-to-pulau-selirong.jpg" + }, + { + "name": "Tee off at the Empire Hotel and Country Club", + "description": "Indulge in a luxurious golfing experience at the Empire Hotel and Country Club, boasting a world-class 18-hole golf course designed by Jack Nicklaus. Enjoy the stunning coastal views as you navigate the challenging course, and afterward, unwind at the opulent hotel facilities.", + "locationName": "Empire Hotel and Country Club", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "brunei", + "ref": "tee-off-at-the-empire-hotel-and-country-club", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/brunei_tee-off-at-the-empire-hotel-and-country-club.jpg" + }, + { + "name": "Soak in the Szechenyi Thermal Baths", + "description": "Experience the ultimate relaxation at the iconic Szechenyi Thermal Baths, one of Europe's largest and most beautiful spa complexes. Immerse yourself in the warm, mineral-rich waters, enjoy a traditional massage, and admire the stunning neo-Baroque architecture.", + "locationName": "Szechenyi Thermal Baths", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "budapest", + "ref": "soak-in-the-szechenyi-thermal-baths", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_soak-in-the-szechenyi-thermal-baths.jpg" + }, + { + "name": "Explore Buda Castle and Fisherman's Bastion", + "description": "Step back in time with a visit to Buda Castle, a historic palace complex offering panoramic views of the city. Wander through the charming medieval streets, admire the Matthias Church, and capture breathtaking photos from the Fisherman's Bastion.", + "locationName": "Buda Castle District", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "budapest", + "ref": "explore-buda-castle-and-fisherman-s-bastion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_explore-buda-castle-and-fisherman-s-bastion.jpg" + }, + { + "name": "Cruise the Danube River at Sunset", + "description": "Embark on a romantic evening cruise along the Danube River and witness the city illuminate as the sun sets. Admire iconic landmarks like the Hungarian Parliament Building and Chain Bridge, while enjoying live music and a delicious dinner on board.", + "locationName": "Danube River", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "budapest", + "ref": "cruise-the-danube-river-at-sunset", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_cruise-the-danube-river-at-sunset.jpg" + }, + { + "name": "Discover Hungarian Cuisine on a Food Tour", + "description": "Embark on a culinary adventure through Budapest's vibrant food scene. Sample traditional Hungarian dishes like goulash, lángos, and chimney cake, and learn about the city's rich culinary history and local favorites.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "budapest", + "ref": "discover-hungarian-cuisine-on-a-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_discover-hungarian-cuisine-on-a-food-tour.jpg" + }, + { + "name": "Visit the Hungarian Parliament Building", + "description": "Marvel at the architectural masterpiece that is the Hungarian Parliament Building, a stunning example of Gothic Revival style. Take a guided tour to learn about its history and admire the intricate details of its interior, including the Grand Staircase and the Hungarian Crown Jewels.", + "locationName": "Hungarian Parliament Building", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "budapest", + "ref": "visit-the-hungarian-parliament-building", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_visit-the-hungarian-parliament-building.jpg" + }, + { + "name": "Stroll Down Andrássy Avenue", + "description": "Take a leisurely walk down Andrássy Avenue, a UNESCO World Heritage Site, and admire the elegant architecture, luxury boutiques, and charming cafes. This grand boulevard, often compared to the Champs-Élysées, offers a glimpse into Budapest's opulent past and vibrant present.", + "locationName": "Andrássy Avenue", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "budapest", + "ref": "stroll-down-andr-ssy-avenue", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_stroll-down-andr-ssy-avenue.jpg" + }, + { + "name": "Visit the House of Terror", + "description": "Delve into Hungary's complex 20th-century history at the House of Terror museum. This thought-provoking museum housed in a former secret police headquarters documents the fascist and communist regimes, offering a powerful and moving experience.", + "locationName": "House of Terror", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "budapest", + "ref": "visit-the-house-of-terror", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_visit-the-house-of-terror.jpg" + }, + { + "name": "Enjoy Ruin Bars in the Jewish Quarter", + "description": "Experience Budapest's unique nightlife scene by exploring the ruin bars in the historic Jewish Quarter. These trendy bars, housed in abandoned buildings, offer a quirky and eclectic atmosphere with live music, art installations, and a vibrant crowd.", + "locationName": "Jewish Quarter", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "budapest", + "ref": "enjoy-ruin-bars-in-the-jewish-quarter", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_enjoy-ruin-bars-in-the-jewish-quarter.jpg" + }, + { + "name": "Explore Margaret Island", + "description": "Escape the city bustle and find tranquility on Margaret Island, a green oasis in the heart of the Danube. Rent a bike, enjoy a picnic, visit the Japanese Garden, or simply relax by the musical fountain.", + "locationName": "Margaret Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "budapest", + "ref": "explore-margaret-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_explore-margaret-island.jpg" + }, + { + "name": "Shop at the Great Market Hall", + "description": "Immerse yourself in the local culture at the Great Market Hall, a bustling marketplace offering a wide array of Hungarian products. Find fresh produce, local crafts, souvenirs, and traditional food specialties under its impressive roof.", + "locationName": "Great Market Hall", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "budapest", + "ref": "shop-at-the-great-market-hall", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_shop-at-the-great-market-hall.jpg" + }, + { + "name": "Take a Day Trip to Szentendre", + "description": "Escape the city bustle and journey to the charming town of Szentendre, a haven for artists and art enthusiasts. Wander through cobblestone streets lined with colorful houses, explore numerous art galleries and studios, and discover unique handcrafted souvenirs. Enjoy a leisurely lunch at a riverside restaurant, soaking in the idyllic atmosphere.", + "locationName": "Szentendre", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "budapest", + "ref": "take-a-day-trip-to-szentendre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_take-a-day-trip-to-szentendre.jpg" + }, + { + "name": "Go Caving Beneath Buda Castle", + "description": "Embark on an underground adventure through the Pálvölgyi-Mátyáshegyi cave system, one of Europe's largest. Discover a hidden world of stalactites, stalagmites, and unique rock formations, while learning about the geological history of the area. This guided tour offers an exciting and educational experience for all ages.", + "locationName": "Pálvölgyi-Mátyáshegyi Caves", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "budapest", + "ref": "go-caving-beneath-buda-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_go-caving-beneath-buda-castle.jpg" + }, + { + "name": "Attend a Performance at the Hungarian State Opera House", + "description": "Experience the grandeur of the Hungarian State Opera House, a masterpiece of Neo-Renaissance architecture. Dress up for a special evening and enjoy a world-class opera or ballet performance in a breathtaking setting. Immerse yourself in the rich cultural heritage of Hungary through this unforgettable artistic experience.", + "locationName": "Hungarian State Opera House", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "budapest", + "ref": "attend-a-performance-at-the-hungarian-state-opera-house", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_attend-a-performance-at-the-hungarian-state-opera-house.jpg" + }, + { + "name": "Go Kayaking on the Danube", + "description": "See Budapest from a different perspective with a kayaking tour on the Danube River. Paddle past iconic landmarks like the Parliament Building and Buda Castle, while enjoying the fresh air and scenic views. This active experience is perfect for those seeking a unique and adventurous way to explore the city.", + "locationName": "Danube River", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "budapest", + "ref": "go-kayaking-on-the-danube", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_go-kayaking-on-the-danube.jpg" + }, + { + "name": "Delve into Hungarian History at the National Museum", + "description": "Embark on a captivating journey through Hungarian history at the Hungarian National Museum. Explore exhibits spanning from the Paleolithic era to the present day, showcasing archaeological artifacts, medieval weaponry, and captivating artwork. Gain insights into the country's rich cultural heritage and significant historical events.", + "locationName": "Hungarian National Museum", + "duration": 2.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "budapest", + "ref": "delve-into-hungarian-history-at-the-national-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_delve-into-hungarian-history-at-the-national-museum.jpg" + }, + { + "name": "Experience the Thrill of a Hungarian Folklore Show", + "description": "Immerse yourself in the vibrant energy of Hungarian folk culture with a captivating folklore show. Witness talented dancers and musicians showcasing traditional dances, music, and costumes, accompanied by the rhythmic sounds of the cimbalom. Enjoy an unforgettable evening of entertainment that celebrates the country's cultural heritage.", + "locationName": "Various venues throughout the city", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "budapest", + "ref": "experience-the-thrill-of-a-hungarian-folklore-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_experience-the-thrill-of-a-hungarian-folklore-show.jpg" + }, + { + "name": "Unwind in the Tranquil Buda Hills", + "description": "Escape the bustling city and venture into the serene Buda Hills. Hike or bike through scenic trails, breathe in the fresh air, and enjoy breathtaking panoramic views of Budapest. Discover hidden historical sites, charming cafes, and the iconic Elizabeth Lookout Tower.", + "locationName": "Buda Hills", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "budapest", + "ref": "unwind-in-the-tranquil-buda-hills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_unwind-in-the-tranquil-buda-hills.jpg" + }, + { + "name": "Indulge in a Hungarian Wine Tasting Experience", + "description": "Embark on a sensory journey through Hungary's renowned wine regions. Visit a local winery or wine bar and savor a selection of exquisite Hungarian wines, from full-bodied reds to crisp whites. Learn about the unique characteristics of each region and the art of winemaking, while enjoying the company of fellow wine enthusiasts.", + "locationName": "Various wineries and wine bars", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "budapest", + "ref": "indulge-in-a-hungarian-wine-tasting-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_indulge-in-a-hungarian-wine-tasting-experience.jpg" + }, + { + "name": "Take a Day Trip to the Picturesque Town of Gödöllő", + "description": "Escape the city and journey to the charming town of Gödöllő, home to the magnificent Royal Palace of Gödöllő. Explore the opulent rooms and sprawling gardens of the palace, once a favorite summer residence of Empress Elisabeth of Austria. Discover the town's rich history and enjoy a leisurely stroll through its quaint streets.", + "locationName": "Gödöllő", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "budapest", + "ref": "take-a-day-trip-to-the-picturesque-town-of-g-d-ll-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/budapest_take-a-day-trip-to-the-picturesque-town-of-g-d-ll-.jpg" + }, + { + "name": "Wine Tasting Tour in Côte de Nuits", + "description": "Embark on a sensory journey through the prestigious Côte de Nuits wine region. Visit renowned vineyards, learn about the winemaking process from passionate vintners, and indulge in tastings of exceptional Pinot Noir and Chardonnay wines. Discover the unique terroir and the secrets behind Burgundy's world-class wines.", + "locationName": "Côte de Nuits vineyards", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "burgundy", + "ref": "wine-tasting-tour-in-c-te-de-nuits", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_wine-tasting-tour-in-c-te-de-nuits.jpg" + }, + { + "name": "Explore the Historic City of Dijon", + "description": "Wander through the charming streets of Dijon, the historic capital of Burgundy. Visit the Palace of the Dukes of Burgundy, a magnificent medieval complex, and admire the Gothic architecture of Dijon Cathedral. Explore the vibrant markets, sample local delicacies like Dijon mustard, and soak up the city's rich cultural heritage.", + "locationName": "Dijon", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "burgundy", + "ref": "explore-the-historic-city-of-dijon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_explore-the-historic-city-of-dijon.jpg" + }, + { + "name": "Cycling through the Vineyards", + "description": "Experience the beauty of the Burgundian countryside on a leisurely bike ride through rolling vineyards. Cycle along quiet roads, passing picturesque villages and charming wineries. Stop for picnics amidst the vines and enjoy breathtaking views of the landscape.", + "locationName": "Burgundian countryside", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "burgundy", + "ref": "cycling-through-the-vineyards", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_cycling-through-the-vineyards.jpg" + }, + { + "name": "Canal Cruise on the Canal de Bourgogne", + "description": "Relax and enjoy the scenic beauty of Burgundy on a leisurely canal cruise. Glide along the tranquil waters of the Canal de Bourgogne, passing through charming villages, lush vineyards, and historic sites. Savor delicious meals on board and admire the peaceful landscapes.", + "locationName": "Canal de Bourgogne", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "burgundy", + "ref": "canal-cruise-on-the-canal-de-bourgogne", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_canal-cruise-on-the-canal-de-bourgogne.jpg" + }, + { + "name": "Visit the Château de La Rochepot", + "description": "Step back in time at the Château de La Rochepot, a stunning medieval castle perched on a hilltop. Explore the castle's ramparts, towers, and dungeons, and admire the panoramic views of the surrounding countryside. Learn about the castle's fascinating history and immerse yourself in the medieval atmosphere.", + "locationName": "Château de La Rochepot", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "burgundy", + "ref": "visit-the-ch-teau-de-la-rochepot", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_visit-the-ch-teau-de-la-rochepot.jpg" + }, + { + "name": "Hot Air Balloon Ride over the Vineyards", + "description": "Soar above the picturesque vineyards of Burgundy in a hot air balloon, enjoying breathtaking panoramic views of rolling hills, charming villages, and sprawling estates. This unforgettable experience offers a unique perspective of the region's beauty and is perfect for a romantic occasion or a special celebration.", + "locationName": "Various locations in Burgundy", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "burgundy", + "ref": "hot-air-balloon-ride-over-the-vineyards", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_hot-air-balloon-ride-over-the-vineyards.jpg" + }, + { + "name": "Truffle Hunting Experience", + "description": "Embark on a guided truffle hunting adventure in the forests of Burgundy, accompanied by expert truffle hunters and their trained dogs. Learn about the fascinating world of truffles, from their growth and harvesting to their culinary uses. This immersive experience is a must for food enthusiasts and nature lovers alike.", + "locationName": "Côte-d'Or or Yonne", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "burgundy", + "ref": "truffle-hunting-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_truffle-hunting-experience.jpg" + }, + { + "name": "Kayaking on the Saône River", + "description": "Paddle along the scenic Saône River, enjoying the tranquility of the water and the beauty of the surrounding landscapes. Observe local wildlife, pass by charming villages, and stop for a picnic lunch on the riverbank. Kayaking is a fantastic way to explore Burgundy at your own pace and enjoy the outdoors.", + "locationName": "Saône River", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "burgundy", + "ref": "kayaking-on-the-sa-ne-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_kayaking-on-the-sa-ne-river.jpg" + }, + { + "name": "Cooking Class with a Local Chef", + "description": "Immerse yourself in the culinary traditions of Burgundy by participating in a cooking class led by a local chef. Learn the secrets of regional dishes, from classic Boeuf Bourguignon to delicate pastries. This hands-on experience is a delightful way to discover the flavors of Burgundy and enhance your culinary skills.", + "locationName": "Various locations in Burgundy", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "burgundy", + "ref": "cooking-class-with-a-local-chef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_cooking-class-with-a-local-chef.jpg" + }, + { + "name": "Stargazing in the Burgundy Countryside", + "description": "Escape the city lights and experience the magic of stargazing in the pristine countryside of Burgundy. Join a guided stargazing tour or find a secluded spot to marvel at the constellations and planets. The clear night skies of Burgundy offer an unforgettable astronomical experience.", + "locationName": "Rural areas of Burgundy", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "burgundy", + "ref": "stargazing-in-the-burgundy-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_stargazing-in-the-burgundy-countryside.jpg" + }, + { + "name": "Hiking in the Morvan Regional Natural Park", + "description": "Escape the vineyards and immerse yourself in the natural beauty of the Morvan Regional Natural Park. Hike through lush forests, past sparkling lakes, and up to panoramic viewpoints. Keep an eye out for diverse wildlife, including deer, foxes, and birds of prey. This is a perfect way to experience the region's tranquil side and enjoy some fresh air.", + "locationName": "Morvan Regional Natural Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "burgundy", + "ref": "hiking-in-the-morvan-regional-natural-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_hiking-in-the-morvan-regional-natural-park.jpg" + }, + { + "name": "Explore the Hospices de Beaune", + "description": "Step back in time at the Hospices de Beaune, a magnificent 15th-century hospital complex. Admire the colorful glazed tile roofs, wander through the historic wards, and discover the fascinating history of this charitable institution. Don't miss the annual wine auction, a renowned event that supports the hospital's ongoing work.", + "locationName": "Beaune", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "burgundy", + "ref": "explore-the-hospices-de-beaune", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_explore-the-hospices-de-beaune.jpg" + }, + { + "name": "Visit the Abbaye de Fontenay", + "description": "Journey to the serene Abbaye de Fontenay, a UNESCO World Heritage Site and a masterpiece of Cistercian architecture. Explore the abbey church, cloister, dormitory, and gardens, and experience the peaceful atmosphere of this historic monastic community.", + "locationName": "Marmagne", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "burgundy", + "ref": "visit-the-abbaye-de-fontenay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_visit-the-abbaye-de-fontenay.jpg" + }, + { + "name": "Indulge in Gourmet Delights", + "description": "Embark on a culinary adventure and savor the rich gastronomy of Burgundy. From Michelin-starred restaurants to charming bistros, the region offers a diverse array of dining experiences. Sample regional specialties such as boeuf bourguignon, coq au vin, and escargots, paired with local wines for an unforgettable feast.", + "locationName": "Various locations throughout Burgundy", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "burgundy", + "ref": "indulge-in-gourmet-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_indulge-in-gourmet-delights.jpg" + }, + { + "name": "Discover the Art and History of Dijon", + "description": "Delve into the cultural heart of Burgundy in Dijon, a vibrant city with a rich artistic and historical legacy. Explore the Palace of the Dukes of Burgundy, admire the Gothic architecture of Notre Dame Church, and wander through the charming streets lined with half-timbered houses. Don't miss the Museum of Fine Arts, home to an impressive collection of European paintings and sculptures.", + "locationName": "Dijon", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "burgundy", + "ref": "discover-the-art-and-history-of-dijon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_discover-the-art-and-history-of-dijon.jpg" + }, + { + "name": "Horseback Riding through the Vineyards", + "description": "Embark on a scenic horseback riding adventure through the picturesque vineyards of Burgundy. Explore the rolling hills, charming villages, and renowned wine estates from a unique perspective. Enjoy the fresh air, stunning landscapes, and the gentle rhythm of horseback riding, creating a memorable and relaxing experience.", + "locationName": "Côte de Beaune or Côte de Nuits", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "burgundy", + "ref": "horseback-riding-through-the-vineyards", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_horseback-riding-through-the-vineyards.jpg" + }, + { + "name": "Visit the Medieval Town of Semur-en-Auxois", + "description": "Step back in time with a visit to the charming medieval town of Semur-en-Auxois. Explore its well-preserved ramparts, cobblestone streets, and half-timbered houses. Discover the impressive Collegiate Church of Notre-Dame and the historic Château de Semur-en-Auxois, offering panoramic views of the surrounding countryside.", + "locationName": "Semur-en-Auxois", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "burgundy", + "ref": "visit-the-medieval-town-of-semur-en-auxois", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_visit-the-medieval-town-of-semur-en-auxois.jpg" + }, + { + "name": "Go Antiquing in Beaune", + "description": "Embark on a treasure hunt through the antique shops and markets of Beaune. Discover unique furniture, vintage jewelry, and other hidden gems that reflect the region's rich history and culture. Enjoy the thrill of the hunt and the satisfaction of finding one-of-a-kind souvenirs to bring home.", + "locationName": "Beaune", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "burgundy", + "ref": "go-antiquing-in-beaune", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_go-antiquing-in-beaune.jpg" + }, + { + "name": "Sample Local Cheeses at a Fromagerie", + "description": "Indulge in the rich flavors of Burgundy's artisanal cheeses at a local fromagerie. Learn about the cheesemaking process, discover different varieties such as Époisses and Soumaintrain, and savor the unique textures and tastes. Pair your cheese with a glass of local wine for a truly delightful experience.", + "locationName": "Various fromageries throughout Burgundy", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "burgundy", + "ref": "sample-local-cheeses-at-a-fromagerie", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_sample-local-cheeses-at-a-fromagerie.jpg" + }, + { + "name": "Enjoy a Picnic in the Countryside", + "description": "Escape the hustle and bustle with a relaxing picnic amidst the idyllic landscapes of Burgundy. Gather fresh bread, local cheeses, cured meats, and a bottle of wine, and find a scenic spot among the vineyards or along a tranquil riverbank. Savor the delicious food, soak up the sunshine, and enjoy the peaceful ambiance.", + "locationName": "Various locations throughout Burgundy", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "burgundy", + "ref": "enjoy-a-picnic-in-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/burgundy_enjoy-a-picnic-in-the-countryside.jpg" + }, + { + "name": "Explore the Temples of Angkor", + "description": "Embark on a journey through time as you explore the magnificent temples of Angkor, a UNESCO World Heritage Site. Marvel at the intricate carvings of Angkor Wat, the world's largest religious monument, and discover the enigmatic faces of Bayon Temple. Wander through the jungle-clad Ta Prohm, where ancient ruins intertwine with nature, and witness the breathtaking sunrise over Angkor Wat.", + "locationName": "Angkor Archaeological Park", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cambodia", + "ref": "explore-the-temples-of-angkor", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_explore-the-temples-of-angkor.jpg" + }, + { + "name": "Relax on the Beaches of Sihanoukville", + "description": "Escape to the pristine beaches of Sihanoukville, where turquoise waters meet golden sands. Bask in the sun, swim in the crystal-clear sea, or indulge in water sports like snorkeling and diving. Explore the vibrant beach bars and restaurants, or simply unwind with a refreshing cocktail as you soak up the tropical atmosphere.", + "locationName": "Sihanoukville", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cambodia", + "ref": "relax-on-the-beaches-of-sihanoukville", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_relax-on-the-beaches-of-sihanoukville.jpg" + }, + { + "name": "Cruise the Mekong River", + "description": "Embark on a scenic cruise along the mighty Mekong River, the lifeblood of Southeast Asia. Witness the daily life of local communities, observe floating villages and vibrant markets, and immerse yourself in the lush landscapes. Opt for a sunset cruise to enjoy breathtaking views as the sky transforms into a canvas of colors.", + "locationName": "Mekong River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cambodia", + "ref": "cruise-the-mekong-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_cruise-the-mekong-river.jpg" + }, + { + "name": "Discover the Royal Palace in Phnom Penh", + "description": "Immerse yourself in the grandeur of the Royal Palace in Phnom Penh, a symbol of Cambodia's rich heritage. Explore the Silver Pagoda, adorned with thousands of silver tiles, and marvel at the Khmer architecture of the Throne Hall. Wander through the manicured gardens and witness the changing of the guards ceremony for a glimpse into royal traditions.", + "locationName": "Phnom Penh", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "cambodia", + "ref": "discover-the-royal-palace-in-phnom-penh", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_discover-the-royal-palace-in-phnom-penh.jpg" + }, + { + "name": "Learn About Cambodian History at the Tuol Sleng Genocide Museum", + "description": "Gain a deeper understanding of Cambodia's turbulent past at the Tuol Sleng Genocide Museum, a former prison that stands as a stark reminder of the Khmer Rouge regime. Explore the exhibits and learn about the atrocities committed during this dark period, paying respects to the victims and reflecting on the importance of human rights.", + "locationName": "Phnom Penh", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 1, + "destinationRef": "cambodia", + "ref": "learn-about-cambodian-history-at-the-tuol-sleng-genocide-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_learn-about-cambodian-history-at-the-tuol-sleng-genocide-museum.jpg" + }, + { + "name": "Hike Through the Cardamom Mountains", + "description": "Embark on an adventurous trek through the lush Cardamom Mountains, home to diverse wildlife, cascading waterfalls, and remote villages. Explore jungle trails, encounter exotic flora and fauna, and discover hidden gems like the Tatai Waterfall or the Aural District.", + "locationName": "Cardamom Mountains", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "cambodia", + "ref": "hike-through-the-cardamom-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_hike-through-the-cardamom-mountains.jpg" + }, + { + "name": "Go Birdwatching in Prek Toal Bird Sanctuary", + "description": "Immerse yourself in the natural wonders of the Prek Toal Bird Sanctuary, a haven for bird enthusiasts. Embark on a boat trip through the flooded forests and witness a spectacular array of bird species, including the endangered Greater Adjutant and the Painted Stork.", + "locationName": "Prek Toal Bird Sanctuary", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cambodia", + "ref": "go-birdwatching-in-prek-toal-bird-sanctuary", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_go-birdwatching-in-prek-toal-bird-sanctuary.jpg" + }, + { + "name": "Experience Local Life at a Homestay", + "description": "Delve into authentic Cambodian culture by staying in a local homestay. Immerse yourself in the daily life of a Cambodian family, learn about their traditions, participate in activities like farming or fishing, and savor home-cooked meals.", + "locationName": "Various rural villages", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "cambodia", + "ref": "experience-local-life-at-a-homestay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_experience-local-life-at-a-homestay.jpg" + }, + { + "name": "Sample Street Food in Phnom Penh", + "description": "Embark on a culinary adventure through the vibrant streets of Phnom Penh. Sample a variety of delicious and affordable street food options, such as Khmer noodles, grilled meats, fresh spring rolls, and sweet treats like sticky rice with mango.", + "locationName": "Phnom Penh", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "cambodia", + "ref": "sample-street-food-in-phnom-penh", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_sample-street-food-in-phnom-penh.jpg" + }, + { + "name": "Learn to Cook Khmer Cuisine", + "description": "Discover the secrets of Khmer cuisine by taking a cooking class. Learn to prepare traditional dishes like fish amok, Khmer curry, and green mango salad, using fresh local ingredients and authentic cooking techniques.", + "locationName": "Various cooking schools", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "cambodia", + "ref": "learn-to-cook-khmer-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_learn-to-cook-khmer-cuisine.jpg" + }, + { + "name": "Visit Koh Rong", + "description": "Escape to the idyllic island of Koh Rong and immerse yourself in its pristine beauty. Relax on white-sand beaches, swim in crystal-clear waters, and explore the vibrant coral reefs through snorkeling or diving. Enjoy the laid-back island atmosphere, indulge in fresh seafood, and witness breathtaking sunsets over the Gulf of Thailand.", + "locationName": "Koh Rong Island", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cambodia", + "ref": "visit-koh-rong", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_visit-koh-rong.jpg" + }, + { + "name": "Go Kayaking on the Kampot River", + "description": "Embark on a serene kayaking adventure along the Kampot River, surrounded by lush mangroves and stunning natural landscapes. Paddle through tranquil waters, observe local wildlife, and discover hidden coves and caves. Witness the captivating beauty of the Cambodian countryside and enjoy a peaceful escape into nature.", + "locationName": "Kampot River", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "cambodia", + "ref": "go-kayaking-on-the-kampot-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_go-kayaking-on-the-kampot-river.jpg" + }, + { + "name": "Explore the Bokor National Park", + "description": "Venture into the Bokor National Park and discover its enchanting beauty and historical remnants. Visit the abandoned Bokor Hill Station, a French colonial resort with stunning views, and explore the Popokvil Waterfall, a cascading wonder amidst the lush rainforest. Hike through scenic trails, encounter diverse wildlife, and immerse yourself in the park's mystical atmosphere.", + "locationName": "Bokor National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "cambodia", + "ref": "explore-the-bokor-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_explore-the-bokor-national-park.jpg" + }, + { + "name": "Shop at the Russian Market in Phnom Penh", + "description": "Immerse yourself in the vibrant atmosphere of the Russian Market, a bustling marketplace in Phnom Penh. Browse through a wide array of goods, including souvenirs, handicrafts, clothing, and local produce. Experience the energetic hustle and bustle of Cambodian commerce and discover unique treasures to take home.", + "locationName": "Russian Market", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "cambodia", + "ref": "shop-at-the-russian-market-in-phnom-penh", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_shop-at-the-russian-market-in-phnom-penh.jpg" + }, + { + "name": "Enjoy Phare Circus Show in Siem Reap", + "description": "Experience the magic of Phare Circus, a captivating performance that combines acrobatics, theater, and Cambodian storytelling. Be amazed by the talented young artists who showcase their skills and share their stories through this unique art form. Enjoy an evening of entertainment and cultural immersion.", + "locationName": "Siem Reap", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "cambodia", + "ref": "enjoy-phare-circus-show-in-siem-reap", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_enjoy-phare-circus-show-in-siem-reap.jpg" + }, + { + "name": "Dive into the Underwater World of Koh Tang", + "description": "Embark on an unforgettable scuba diving or snorkeling adventure to Koh Tang, a remote island paradise renowned for its pristine coral reefs and diverse marine life. Explore vibrant underwater landscapes, encounter colorful fish, and discover hidden coves, making memories that will last a lifetime.", + "locationName": "Koh Tang", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "cambodia", + "ref": "dive-into-the-underwater-world-of-koh-tang", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_dive-into-the-underwater-world-of-koh-tang.jpg" + }, + { + "name": "Witness the Serenity of Phnom Kulen National Park", + "description": "Escape the hustle and bustle of city life and immerse yourself in the tranquil beauty of Phnom Kulen National Park. Hike to the cascading Kulen Waterfall, discover ancient temples hidden within the lush jungle, and take a refreshing dip in the sacred River of a Thousand Lingas, experiencing the spiritual heart of Cambodia.", + "locationName": "Phnom Kulen National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cambodia", + "ref": "witness-the-serenity-of-phnom-kulen-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_witness-the-serenity-of-phnom-kulen-national-park.jpg" + }, + { + "name": "Delve into Rural Life in Battambang", + "description": "Venture beyond the tourist trail and experience authentic Cambodian life in the charming province of Battambang. Take a ride on the iconic Bamboo Train, explore traditional villages, visit ancient temples, and witness the breathtaking sunset over rice paddies, gaining a deeper understanding of the local culture and way of life.", + "locationName": "Battambang", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cambodia", + "ref": "delve-into-rural-life-in-battambang", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_delve-into-rural-life-in-battambang.jpg" + }, + { + "name": "Unwind in Kep's Coastal Charm", + "description": "Discover the laid-back coastal town of Kep, known for its fresh seafood, serene beaches, and colonial architecture. Relax on the white sand, indulge in a delicious crab market feast, and explore nearby Rabbit Island for a dose of tranquility and stunning ocean views.", + "locationName": "Kep", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cambodia", + "ref": "unwind-in-kep-s-coastal-charm", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_unwind-in-kep-s-coastal-charm.jpg" + }, + { + "name": "Step Back in Time at Koh Ker Temple Complex", + "description": "Embark on an archaeological adventure to the Koh Ker Temple Complex, a remote and enigmatic site that once served as the capital of the Khmer Empire. Explore towering temples, intricate carvings, and hidden chambers, feeling the aura of mystery and grandeur that surrounds this ancient wonder.", + "locationName": "Koh Ker Temple Complex", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "cambodia", + "ref": "step-back-in-time-at-koh-ker-temple-complex", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cambodia_step-back-in-time-at-koh-ker-temple-complex.jpg" + }, + { + "name": "Hiking in Banff National Park", + "description": "Explore the breathtaking trails of Banff National Park, from easy lakeside strolls to challenging mountain climbs. Discover iconic spots like Lake Louise, Moraine Lake, and Johnston Canyon, and be rewarded with stunning vistas of turquoise waters, glaciers, and snow-capped peaks. Keep an eye out for wildlife such as elk, deer, and bighorn sheep.", + "locationName": "Banff National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "canadian-rockies", + "ref": "hiking-in-banff-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_hiking-in-banff-national-park.jpg" + }, + { + "name": "Skiing or Snowboarding in Lake Louise", + "description": "Hit the slopes at Lake Louise Ski Resort, renowned for its world-class skiing and snowboarding terrain. With over 4,200 acres of skiable area, there's something for all levels, from gentle beginner runs to thrilling black diamond slopes. Enjoy the incredible views of the surrounding mountains and the frozen lake while carving down the powder.", + "locationName": "Lake Louise Ski Resort", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "canadian-rockies", + "ref": "skiing-or-snowboarding-in-lake-louise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_skiing-or-snowboarding-in-lake-louise.jpg" + }, + { + "name": "Scenic Drive on the Icefields Parkway", + "description": "Embark on a breathtaking road trip along the Icefields Parkway, considered one of the most scenic drives in the world. Marvel at the towering mountains, glaciers, waterfalls, and turquoise lakes that line the route. Stop at iconic viewpoints like Bow Lake, Peyto Lake, and the Columbia Icefield, and take in the awe-inspiring beauty of the Canadian Rockies.", + "locationName": "Icefields Parkway", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "canadian-rockies", + "ref": "scenic-drive-on-the-icefields-parkway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_scenic-drive-on-the-icefields-parkway.jpg" + }, + { + "name": "Wildlife Watching Tour", + "description": "Join a guided wildlife watching tour to increase your chances of spotting iconic Canadian Rockies animals such as grizzly bears, black bears, elk, moose, bighorn sheep, and mountain goats. Learn about their behavior, habitats, and conservation efforts from experienced guides, and capture unforgettable photos of these majestic creatures in their natural environment.", + "locationName": "Banff National Park or Jasper National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "canadian-rockies", + "ref": "wildlife-watching-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_wildlife-watching-tour.jpg" + }, + { + "name": "Relaxing at Banff Upper Hot Springs", + "description": "After a day of outdoor adventures, unwind and rejuvenate at the Banff Upper Hot Springs. Immerse yourself in the soothing mineral-rich waters, surrounded by stunning mountain scenery. Enjoy the therapeutic benefits of the hot springs while taking in the fresh mountain air and breathtaking views.", + "locationName": "Banff Upper Hot Springs", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "canadian-rockies", + "ref": "relaxing-at-banff-upper-hot-springs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_relaxing-at-banff-upper-hot-springs.jpg" + }, + { + "name": "Whitewater Rafting on the Kicking Horse River", + "description": "Experience the thrill of whitewater rafting on the Kicking Horse River, known for its exhilarating rapids and stunning scenery. Choose from various trip lengths and intensities to suit your adventure level, and enjoy a day of splashing through rapids, surrounded by the beauty of the Canadian Rockies. #Adventure #Summer #Family-friendly", + "locationName": "Kicking Horse River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "canadian-rockies", + "ref": "whitewater-rafting-on-the-kicking-horse-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_whitewater-rafting-on-the-kicking-horse-river.jpg" + }, + { + "name": "Gondola Ride to the Top of Sulphur Mountain", + "description": "Take a scenic gondola ride to the summit of Sulphur Mountain for breathtaking panoramic views of the surrounding mountains, valleys, and the town of Banff. Enjoy the interpretive exhibits at the top, dine at the mountaintop restaurant, or simply soak in the awe-inspiring vistas. #Sightseeing #Mountain #Family-friendly", + "locationName": "Sulphur Mountain", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "canadian-rockies", + "ref": "gondola-ride-to-the-top-of-sulphur-mountain", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_gondola-ride-to-the-top-of-sulphur-mountain.jpg" + }, + { + "name": "Explore the Charming Town of Banff", + "description": "Wander through the charming streets of Banff, a picturesque mountain town with a vibrant atmosphere. Discover unique shops, art galleries, and museums, or relax at a cozy café and enjoy the mountain views. #Shopping #Cultural #Relaxing", + "locationName": "Banff", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "canadian-rockies", + "ref": "explore-the-charming-town-of-banff", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_explore-the-charming-town-of-banff.jpg" + }, + { + "name": "Visit the Cave and Basin National Historic Site", + "description": "Delve into the history of Banff National Park at the Cave and Basin National Historic Site, where Canada's first national park was established. Explore the natural hot springs, learn about the area's unique geology and ecology, and discover the cultural significance of this landmark site. #Historic #Cultural #Family-friendly", + "locationName": "Cave and Basin National Historic Site", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "canadian-rockies", + "ref": "visit-the-cave-and-basin-national-historic-site", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_visit-the-cave-and-basin-national-historic-site.jpg" + }, + { + "name": "Stargazing in the Dark Sky Preserves", + "description": "Escape the city lights and experience the magic of stargazing in the Canadian Rockies' Dark Sky Preserves. Join a guided astronomy tour or simply find a secluded spot to marvel at the constellations and the Milky Way. #Nightlife #Secluded #Romantic", + "locationName": "Jasper National Park or Banff National Park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "canadian-rockies", + "ref": "stargazing-in-the-dark-sky-preserves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_stargazing-in-the-dark-sky-preserves.jpg" + }, + { + "name": "Dog Sledding Adventure", + "description": "Experience the thrill of gliding through the snowy landscapes of the Canadian Rockies on a dog sledding tour. Feel the crisp mountain air as a team of huskies pulls you across frozen lakes and through pristine forests. Learn about the history of dog sledding and the incredible bond between mushers and their canine companions.", + "locationName": "Banff or Canmore", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "canadian-rockies", + "ref": "dog-sledding-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_dog-sledding-adventure.jpg" + }, + { + "name": "Ice Skating on Lake Louise", + "description": "Lace up your skates and glide across the frozen surface of the iconic Lake Louise. Surrounded by towering mountains and the majestic Victoria Glacier, this is a truly unforgettable experience. Enjoy the fresh air and stunning scenery as you create lasting memories on the ice.", + "locationName": "Lake Louise", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "canadian-rockies", + "ref": "ice-skating-on-lake-louise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_ice-skating-on-lake-louise.jpg" + }, + { + "name": "Snowshoeing in Yoho National Park", + "description": "Embark on a peaceful snowshoeing adventure through the winter wonderland of Yoho National Park. Explore snow-covered trails, admire frozen waterfalls, and discover hidden alpine meadows. This is a great way to experience the serenity of the mountains at your own pace.", + "locationName": "Yoho National Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "canadian-rockies", + "ref": "snowshoeing-in-yoho-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_snowshoeing-in-yoho-national-park.jpg" + }, + { + "name": "Johnston Canyon Icewalk", + "description": "Venture into the depths of Johnston Canyon and marvel at the frozen waterfalls and ice formations that cling to the canyon walls. Guided tours provide insights into the geology and ecology of the area, making this a unique and educational experience.", + "locationName": "Johnston Canyon", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "canadian-rockies", + "ref": "johnston-canyon-icewalk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_johnston-canyon-icewalk.jpg" + }, + { + "name": "Indigenous Cultural Experiences", + "description": "Learn about the rich history and culture of the Indigenous peoples who have called the Canadian Rockies home for centuries. Visit cultural centers, participate in traditional storytelling sessions, or try your hand at crafting authentic Indigenous art.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "canadian-rockies", + "ref": "indigenous-cultural-experiences", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_indigenous-cultural-experiences.jpg" + }, + { + "name": "Mountain Biking Adventure", + "description": "Embark on an exhilarating mountain biking adventure along scenic trails with breathtaking views of the surrounding peaks and valleys. Numerous trails cater to all skill levels, from leisurely paths to challenging single tracks, offering an unforgettable experience for outdoor enthusiasts.", + "locationName": "Various locations throughout the Canadian Rockies", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "canadian-rockies", + "ref": "mountain-biking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_mountain-biking-adventure.jpg" + }, + { + "name": "Rock Climbing Excursion", + "description": "Challenge yourself with a thrilling rock climbing excursion on the iconic cliffs and rock faces of the Canadian Rockies. Whether you're a seasoned climber or a beginner, experienced guides will lead you on an unforgettable adventure, providing instruction and ensuring your safety as you ascend to new heights.", + "locationName": "Popular climbing areas like Yamnuska and Grassi Lakes", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "canadian-rockies", + "ref": "rock-climbing-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_rock-climbing-excursion.jpg" + }, + { + "name": "Wildlife Photography Safari", + "description": "Embark on a wildlife photography safari to capture stunning images of the diverse fauna that inhabits the Canadian Rockies. Accompanied by expert guides, you'll have the opportunity to spot elusive creatures like grizzly bears, elk, moose, and bighorn sheep in their natural habitat.", + "locationName": "National parks and wildlife reserves", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "canadian-rockies", + "ref": "wildlife-photography-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_wildlife-photography-safari.jpg" + }, + { + "name": "Scenic Helicopter Tour", + "description": "Soar above the majestic peaks and turquoise lakes of the Canadian Rockies on a scenic helicopter tour. Witness the breathtaking beauty of the landscape from a unique perspective, capturing panoramic views of glaciers, waterfalls, and alpine meadows.", + "locationName": "Helicopter tour operators in Banff or Canmore", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "canadian-rockies", + "ref": "scenic-helicopter-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_scenic-helicopter-tour.jpg" + }, + { + "name": "Relaxing Spa Day", + "description": "Indulge in a day of relaxation and rejuvenation at a luxurious spa nestled amidst the stunning scenery of the Canadian Rockies. Treat yourself to a variety of treatments, including massages, facials, and body wraps, while enjoying the serene ambiance and breathtaking views.", + "locationName": "Spas in Banff, Lake Louise, or Jasper", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "canadian-rockies", + "ref": "relaxing-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canadian-rockies_relaxing-spa-day.jpg" + }, + { + "name": "Hike Mount Teide", + "description": "Embark on a challenging but rewarding hike to the summit of Mount Teide, Spain's highest peak and a dormant volcano. Witness breathtaking panoramic views of the island and surrounding archipelago from above. You can choose to hike or take a cable car to the top.", + "locationName": "Teide National Park, Tenerife", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "canary-islands", + "ref": "hike-mount-teide", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_hike-mount-teide.jpg" + }, + { + "name": "Relax on the Beaches of Gran Canaria", + "description": "Soak up the sun and enjoy the golden sands of Gran Canaria's famous beaches. From the lively Playa del Inglés to the secluded coves of Güi Güi beach, there's a perfect spot for everyone to unwind and enjoy the crystal-clear waters.", + "locationName": "Gran Canaria", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "canary-islands", + "ref": "relax-on-the-beaches-of-gran-canaria", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_relax-on-the-beaches-of-gran-canaria.jpg" + }, + { + "name": "Stargazing in La Palma", + "description": "Experience the magic of the night sky in La Palma, a designated Starlight Reserve. Join a guided stargazing tour and marvel at the constellations, planets, and distant galaxies with minimal light pollution.", + "locationName": "Roque de los Muchachos Observatory, La Palma", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "canary-islands", + "ref": "stargazing-in-la-palma", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_stargazing-in-la-palma.jpg" + }, + { + "name": "Explore the Timanfaya National Park", + "description": "Discover the otherworldly landscapes of Timanfaya National Park in Lanzarote. Witness volcanic craters, lava fields, and geothermal demonstrations, and enjoy a unique camel ride through this Martian-like environment.", + "locationName": "Timanfaya National Park, Lanzarote", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "canary-islands", + "ref": "explore-the-timanfaya-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_explore-the-timanfaya-national-park.jpg" + }, + { + "name": "Go Surfing in Fuerteventura", + "description": "Catch some waves in Fuerteventura, a renowned surfing destination. With consistent winds and diverse breaks, the island offers opportunities for surfers of all levels, from beginners to experienced riders.", + "locationName": "Corralejo or El Cotillo, Fuerteventura", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "canary-islands", + "ref": "go-surfing-in-fuerteventura", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_go-surfing-in-fuerteventura.jpg" + }, + { + "name": "Caving in Cueva de los Verdes", + "description": "Embark on a subterranean adventure by exploring the Cueva de los Verdes, a unique lava tube formed by volcanic eruptions thousands of years ago. Marvel at the impressive geological formations, hidden chambers, and the natural auditorium with its exceptional acoustics, where concerts are occasionally held. This otherworldly experience is a must for curious minds and adventure seekers.", + "locationName": "Lanzarote", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "canary-islands", + "ref": "caving-in-cueva-de-los-verdes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_caving-in-cueva-de-los-verdes.jpg" + }, + { + "name": "Whale and Dolphin Watching", + "description": "Set sail on a boat tour from Tenerife or Gran Canaria and witness the incredible marine life that inhabits the waters surrounding the Canary Islands. Keep an eye out for playful dolphins, majestic whales, and other fascinating creatures like sea turtles and flying fish. This eco-friendly activity is perfect for nature lovers and families with children.", + "locationName": "Tenerife or Gran Canaria", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "canary-islands", + "ref": "whale-and-dolphin-watching", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_whale-and-dolphin-watching.jpg" + }, + { + "name": "Explore the Historic Town of La Laguna", + "description": "Step back in time by visiting the charming town of La Laguna in Tenerife, a UNESCO World Heritage Site. Wander through its cobblestone streets lined with colorful colonial architecture, historical churches, and lively plazas. Discover the local culture, indulge in delicious Canarian cuisine, and soak up the vibrant atmosphere of this historic gem.", + "locationName": "Tenerife", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "canary-islands", + "ref": "explore-the-historic-town-of-la-laguna", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_explore-the-historic-town-of-la-laguna.jpg" + }, + { + "name": "Scuba Diving or Snorkeling Adventure", + "description": "Dive into the crystal-clear waters of the Canary Islands and discover a vibrant underwater world teeming with marine life. Explore volcanic reefs, underwater caves, and shipwrecks while encountering colorful fish, graceful rays, and other fascinating creatures. Whether you are an experienced diver or a beginner snorkeler, the Canary Islands offer unforgettable underwater experiences.", + "locationName": "Various islands", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "canary-islands", + "ref": "scuba-diving-or-snorkeling-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_scuba-diving-or-snorkeling-adventure.jpg" + }, + { + "name": "Indulge in Canarian Cuisine", + "description": "Embark on a culinary journey through the Canary Islands by savoring its unique and flavorful cuisine. Sample local delicacies like papas arrugadas (wrinkled potatoes) with mojo sauce, fresh seafood dishes, hearty stews, and sweet treats like bienmesabe (almond dessert). Explore traditional restaurants, tapas bars, and local markets to experience the rich gastronomy of the islands.", + "locationName": "Various islands", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "canary-islands", + "ref": "indulge-in-canarian-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_indulge-in-canarian-cuisine.jpg" + }, + { + "name": "Kayaking and Cliff Jumping in Los Gigantes", + "description": "Embark on a thrilling kayaking adventure along the towering cliffs of Los Gigantes. Paddle through crystal-clear waters, explore hidden caves, and if you dare, experience the exhilaration of cliff jumping into the refreshing ocean.", + "locationName": "Los Gigantes, Tenerife", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "canary-islands", + "ref": "kayaking-and-cliff-jumping-in-los-gigantes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_kayaking-and-cliff-jumping-in-los-gigantes.jpg" + }, + { + "name": "Sunset Cruise with Dolphin Watching", + "description": "Set sail on a magical sunset cruise along the coast. As the sun dips below the horizon, painting the sky with vibrant hues, keep an eye out for playful dolphins dancing in the waves. Enjoy breathtaking views and create unforgettable memories.", + "locationName": "Various locations across the islands", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "canary-islands", + "ref": "sunset-cruise-with-dolphin-watching", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_sunset-cruise-with-dolphin-watching.jpg" + }, + { + "name": "Jeep Safari Adventure in the Dunes of Maspalomas", + "description": "Embark on an adventurous jeep safari through the mesmerizing dunes of Maspalomas. Traverse the vast desert landscape, feeling the thrill of the ride as you conquer challenging terrains. Discover hidden oases and enjoy panoramic views.", + "locationName": "Maspalomas Dunes, Gran Canaria", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "canary-islands", + "ref": "jeep-safari-adventure-in-the-dunes-of-maspalomas", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_jeep-safari-adventure-in-the-dunes-of-maspalomas.jpg" + }, + { + "name": "Wine Tasting Tour in Lanzarote's La Geria", + "description": "Indulge in the unique flavors of Canarian wines with a delightful wine tasting tour in La Geria. Explore the volcanic vineyards, learn about the island's distinctive winemaking techniques, and savor a variety of local wines amidst breathtaking scenery.", + "locationName": "La Geria, Lanzarote", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "canary-islands", + "ref": "wine-tasting-tour-in-lanzarote-s-la-geria", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_wine-tasting-tour-in-lanzarote-s-la-geria.jpg" + }, + { + "name": "Paragliding over Tenerife's Coastline", + "description": "Soar through the skies and witness the beauty of Tenerife's coastline from a bird's-eye view with a thrilling paragliding experience. Take off from the mountains and glide over stunning beaches, dramatic cliffs, and charming villages, creating memories that will last a lifetime.", + "locationName": "Various locations in Tenerife", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "canary-islands", + "ref": "paragliding-over-tenerife-s-coastline", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_paragliding-over-tenerife-s-coastline.jpg" + }, + { + "name": "Visit the Palmitos Park", + "description": "Immerse yourself in the wonders of nature at Palmitos Park, a botanical garden and zoo nestled in Gran Canaria. Encounter exotic birds, playful dolphins, colorful fish, and fascinating reptiles. Enjoy captivating shows featuring birds of prey and dolphins, and explore lush gardens showcasing diverse plant life.", + "locationName": "Palmitos Park, Gran Canaria", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "canary-islands", + "ref": "visit-the-palmitos-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_visit-the-palmitos-park.jpg" + }, + { + "name": "Explore the Charming Villages", + "description": "Step back in time as you wander through the charming villages scattered across the Canary Islands. Visit Teror in Gran Canaria with its traditional Canarian architecture and balconies, or the picturesque village of Betancuria in Fuerteventura, known for its historical significance and whitewashed houses. Discover local crafts, enjoy traditional cuisine, and experience the authentic island life.", + "locationName": "Various villages across the islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "canary-islands", + "ref": "explore-the-charming-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_explore-the-charming-villages.jpg" + }, + { + "name": "Take a Catamaran Trip to La Graciosa", + "description": "Embark on a relaxing catamaran trip to La Graciosa, a small, unspoiled island off the coast of Lanzarote. Enjoy the scenic journey, soak up the sun on pristine beaches, and swim in crystal-clear turquoise waters. Explore the island's charming village, Caleta de Sebo, and discover its peaceful atmosphere.", + "locationName": "La Graciosa Island", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "canary-islands", + "ref": "take-a-catamaran-trip-to-la-graciosa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_take-a-catamaran-trip-to-la-graciosa.jpg" + }, + { + "name": "Go Windsurfing or Kitesurfing", + "description": "Experience the thrill of windsurfing or kitesurfing in the Canary Islands, renowned for their ideal wind conditions. Head to Fuerteventura or Tenerife, known as hotspots for these water sports. Whether you're a beginner or an experienced rider, you'll find perfect spots to catch the wind and ride the waves.", + "locationName": "Fuerteventura or Tenerife", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "canary-islands", + "ref": "go-windsurfing-or-kitesurfing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_go-windsurfing-or-kitesurfing.jpg" + }, + { + "name": "Enjoy the Nightlife", + "description": "Experience the vibrant nightlife scene in the Canary Islands, particularly in Tenerife and Gran Canaria. Dance the night away at lively clubs, enjoy live music at bars, or sip cocktails at beachfront lounges. Discover a diverse range of entertainment options, from traditional Canarian music to international DJs.", + "locationName": "Tenerife or Gran Canaria", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "canary-islands", + "ref": "enjoy-the-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/canary-islands_enjoy-the-nightlife.jpg" + }, + { + "name": "Hot Air Balloon Ride Over Cappadocia", + "description": "Embark on a magical hot air balloon ride at sunrise and witness the breathtaking landscape of Cappadocia from above. Drift over the fairy chimneys, valleys, and rock formations as the golden light paints the scenery. This unforgettable experience offers stunning panoramic views and a unique perspective of the region's geological wonders.", + "locationName": "Goreme", + "duration": 1.5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "cappadocia", + "ref": "hot-air-balloon-ride-over-cappadocia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_hot-air-balloon-ride-over-cappadocia.jpg" + }, + { + "name": "Explore the Kaymakli Underground City", + "description": "Descend into the depths of Kaymakli Underground City, an intricate network of tunnels and chambers carved into the soft rock. Discover the fascinating history and ingenuity of the ancient inhabitants as you explore living quarters, stables, kitchens, and ventilation shafts. This subterranean adventure offers a glimpse into a unique way of life.", + "locationName": "Kaymakli", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cappadocia", + "ref": "explore-the-kaymakli-underground-city", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_explore-the-kaymakli-underground-city.jpg" + }, + { + "name": "Hike through the Valley of Love", + "description": "Embark on a scenic hike through the Valley of Love, known for its phallic-shaped rock formations and panoramic views. The trail winds through vineyards, orchards, and fairy chimneys, offering opportunities for stunning photos and a connection with nature. Enjoy a picnic lunch amidst the unique landscape and soak in the tranquility of the valley.", + "locationName": "Goreme National Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "cappadocia", + "ref": "hike-through-the-valley-of-love", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_hike-through-the-valley-of-love.jpg" + }, + { + "name": "Visit the Goreme Open-Air Museum", + "description": "Step back in time at the Goreme Open-Air Museum, a UNESCO World Heritage site showcasing a complex of rock-cut churches and monasteries dating back to the Byzantine era. Admire the well-preserved frescoes depicting biblical scenes and learn about the history of early Christian communities in Cappadocia.", + "locationName": "Goreme", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cappadocia", + "ref": "visit-the-goreme-open-air-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_visit-the-goreme-open-air-museum.jpg" + }, + { + "name": "Indulge in a Turkish Cooking Class", + "description": "Immerse yourself in the culinary culture of Turkey by participating in a hands-on cooking class. Learn to prepare traditional dishes such as manti (Turkish dumplings), dolma (stuffed vegetables), and baklava under the guidance of a local chef. Enjoy the fruits of your labor with a delicious meal and gain valuable insights into Turkish cuisine.", + "locationName": "Goreme or Uchisar", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "cappadocia", + "ref": "indulge-in-a-turkish-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_indulge-in-a-turkish-cooking-class.jpg" + }, + { + "name": "Horseback Riding through the Valleys", + "description": "Embark on a horseback riding adventure through the captivating valleys of Cappadocia. Traverse the unique landscape, passing by fairy chimneys and hidden cave dwellings, while enjoying the tranquility and fresh air. This activity is suitable for all skill levels, offering a unique perspective of the region's beauty.", + "locationName": "Various valleys in Cappadocia", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cappadocia", + "ref": "horseback-riding-through-the-valleys", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_horseback-riding-through-the-valleys.jpg" + }, + { + "name": "Pottery Workshop in Avanos", + "description": "Immerse yourself in the artistic heritage of Cappadocia by participating in a pottery workshop in Avanos, a town renowned for its ceramics. Learn about the traditional techniques from local artisans and create your own unique piece of pottery to take home as a souvenir. This hands-on experience is perfect for those seeking a creative and cultural activity.", + "locationName": "Avanos", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cappadocia", + "ref": "pottery-workshop-in-avanos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_pottery-workshop-in-avanos.jpg" + }, + { + "name": "Sunset ATV Tour", + "description": "Experience the thrill of an ATV adventure through the valleys of Cappadocia as the sun sets, casting a golden glow over the fairy chimneys. Navigate the rugged terrain and enjoy breathtaking panoramic views of the landscape. This exhilarating activity is perfect for adventure seekers and photography enthusiasts.", + "locationName": "Various valleys in Cappadocia", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "cappadocia", + "ref": "sunset-atv-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_sunset-atv-tour.jpg" + }, + { + "name": "Whirling Dervishes Ceremony", + "description": "Witness the mesmerizing Sema ceremony, a spiritual Sufi tradition performed by the Whirling Dervishes. Be captivated by the rhythmic movements and enchanting music as the dervishes spin in a symbolic representation of their journey to divine love. This cultural experience offers a glimpse into the rich history and spirituality of the region.", + "locationName": "Various cultural centers in Cappadocia", + "duration": 1, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "cappadocia", + "ref": "whirling-dervishes-ceremony", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_whirling-dervishes-ceremony.jpg" + }, + { + "name": "Turkish Night with Dinner and Folk Dances", + "description": "Immerse yourself in Turkish culture with a lively evening of traditional food, music, and dance. Enjoy a delicious dinner featuring local specialties while being entertained by folk dancers performing vibrant routines. This experience offers a taste of Turkish hospitality and is a great way to socialize with other travelers.", + "locationName": "Various restaurants and cultural centers in Cappadocia", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "cappadocia", + "ref": "turkish-night-with-dinner-and-folk-dances", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_turkish-night-with-dinner-and-folk-dances.jpg" + }, + { + "name": "Sunrise Photography Tour", + "description": "Capture the breathtaking beauty of Cappadocia's landscape bathed in the golden hues of sunrise. Join a photography tour led by a local expert who will guide you to the best vantage points for stunning photos of the fairy chimneys, valleys, and hot air balloons dotting the sky. Learn tips and tricks to enhance your photography skills while witnessing a magical start to the day. Great for photography, Instagrammable", + "locationName": "Various locations throughout Cappadocia", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "cappadocia", + "ref": "sunrise-photography-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_sunrise-photography-tour.jpg" + }, + { + "name": "Hike to the Ihlara Valley", + "description": "Embark on a scenic hike through the Ihlara Valley, a lush oasis carved by the Melendiz River. Explore hidden rock-cut churches adorned with Byzantine frescoes, marvel at the towering cliffs, and enjoy a peaceful escape into nature. This moderate hike is suitable for various fitness levels and offers a refreshing break from the bustling tourist sites. Hiking, Secluded, Off-the-beaten-path", + "locationName": "Ihlara Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cappadocia", + "ref": "hike-to-the-ihlara-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_hike-to-the-ihlara-valley.jpg" + }, + { + "name": "Visit the Guray Ceramic Museum", + "description": "Delve into the world of Turkish ceramics at the Guray Ceramic Museum in Avanos. Admire a vast collection of traditional pottery, learn about the history and techniques of this ancient craft, and even try your hand at creating your own ceramic masterpiece during a workshop. This immersive experience offers a unique glimpse into Cappadocia's artistic heritage. Cultural experiences, Family-friendly", + "locationName": "Guray Ceramic Museum, Avanos", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cappadocia", + "ref": "visit-the-guray-ceramic-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_visit-the-guray-ceramic-museum.jpg" + }, + { + "name": "Wine Tasting in Urgup", + "description": "Indulge in the rich flavors of Cappadocia's wine region with a visit to a local winery in Urgup. Sample a variety of unique wines made from indigenous grapes, learn about the winemaking process, and savor delicious pairings with regional cheeses and snacks. Wine tasting, Cultural experiences, Foodie", + "locationName": "Wineries in Urgup", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "cappadocia", + "ref": "wine-tasting-in-urgup", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_wine-tasting-in-urgup.jpg" + }, + { + "name": "Relax at a Traditional Turkish Bath (Hammam)", + "description": "Unwind and rejuvenate with a traditional Turkish bath experience. Immerse yourself in the steamy atmosphere, enjoy a relaxing scrub and massage, and emerge feeling refreshed and revitalized. This cultural tradition offers a unique way to pamper yourself and experience authentic Turkish hospitality. Wellness retreats, Relaxing", + "locationName": "Various hammams throughout Cappadocia", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cappadocia", + "ref": "relax-at-a-traditional-turkish-bath-hammam-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_relax-at-a-traditional-turkish-bath-hammam-.jpg" + }, + { + "name": "Rock Climbing and Rappelling Adventure", + "description": "Experience the thrill of rock climbing and rappelling amidst the unique rock formations of Cappadocia. Professional guides will lead you on an unforgettable adventure, suitable for both beginners and experienced climbers, as you ascend the volcanic cliffs and rappel down into hidden canyons. Enjoy breathtaking views and a sense of accomplishment as you conquer the challenges of the Cappadocian landscape.", + "locationName": "Uchisar Valley", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "cappadocia", + "ref": "rock-climbing-and-rappelling-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_rock-climbing-and-rappelling-adventure.jpg" + }, + { + "name": "Jeep Safari through the Valleys", + "description": "Embark on an exhilarating jeep safari through the rugged valleys of Cappadocia. Discover hidden gems and off-the-beaten-path locations, including ancient cave churches, abandoned villages, and panoramic viewpoints. Your knowledgeable guide will share fascinating insights into the region's history and geology, making this a thrilling and educational experience.", + "locationName": "Soganli Valley", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cappadocia", + "ref": "jeep-safari-through-the-valleys", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_jeep-safari-through-the-valleys.jpg" + }, + { + "name": "Stargazing in the Dark Sky Reserve", + "description": "Escape the city lights and immerse yourself in the celestial wonders of Cappadocia. Join a guided stargazing tour in the Goreme National Park, a designated Dark Sky Reserve. With the help of powerful telescopes and expert astronomers, witness the Milky Way, constellations, and distant planets in all their glory.", + "locationName": "Goreme National Park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "cappadocia", + "ref": "stargazing-in-the-dark-sky-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_stargazing-in-the-dark-sky-reserve.jpg" + }, + { + "name": "Visit the Zelve Open Air Museum", + "description": "Explore the fascinating Zelve Open Air Museum, a unique historical site showcasing cave dwellings, churches, and monasteries carved into the volcanic rock. Learn about the lives of early Christians and the region's rich cultural heritage as you wander through this ancient settlement. Discover hidden passages, rock-cut mills, and breathtaking views of the surrounding valleys.", + "locationName": "Zelve Open Air Museum", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "cappadocia", + "ref": "visit-the-zelve-open-air-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_visit-the-zelve-open-air-museum.jpg" + }, + { + "name": "Soak in a Natural Hot Spring", + "description": "Relax and rejuvenate in the therapeutic waters of a natural hot spring. Cappadocia boasts several thermal springs known for their healing properties. Pamper yourself with a traditional Turkish bath experience or simply enjoy the soothing warmth of the mineral-rich waters while surrounded by the stunning Cappadocian landscape.", + "locationName": "Bayramhacili", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cappadocia", + "ref": "soak-in-a-natural-hot-spring", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cappadocia_soak-in-a-natural-hot-spring.jpg" + }, + { + "name": "Hike in Vicente Pérez Rosales National Park", + "description": "Immerse yourself in the stunning landscapes of Vicente Pérez Rosales National Park, home to Petrohué Waterfalls, Osorno Volcano, and Todos los Santos Lake. Hike through ancient forests, witness cascading waterfalls, and be captivated by panoramic views of snow-capped peaks.", + "locationName": "Vicente Pérez Rosales National Park", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "chilean-lake-district", + "ref": "hike-in-vicente-p-rez-rosales-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_hike-in-vicente-p-rez-rosales-national-park.jpg" + }, + { + "name": "Kayak on Lake Llanquihue", + "description": "Embark on a kayaking adventure on the pristine waters of Lake Llanquihue, the second largest lake in Chile. Paddle along the shoreline, surrounded by breathtaking mountain vistas and lush greenery. Keep an eye out for diverse birdlife and enjoy the tranquility of the lake.", + "locationName": "Lake Llanquihue", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "chilean-lake-district", + "ref": "kayak-on-lake-llanquihue", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_kayak-on-lake-llanquihue.jpg" + }, + { + "name": "Explore the German heritage of Frutillar", + "description": "Step back in time and discover the charming town of Frutillar, known for its German colonial architecture and cultural heritage. Visit the German Colonial Museum, admire the traditional wooden houses, and indulge in delicious German pastries and kuchen.", + "locationName": "Frutillar", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "chilean-lake-district", + "ref": "explore-the-german-heritage-of-frutillar", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_explore-the-german-heritage-of-frutillar.jpg" + }, + { + "name": "Relax in the Termas Geometricas Hot Springs", + "description": "Unwind and rejuvenate in the natural hot springs of Termas Geometricas. Immerse yourself in the therapeutic mineral-rich waters, surrounded by a unique architectural design of wooden walkways and lush vegetation. Experience ultimate relaxation amidst the serene natural beauty.", + "locationName": "Termas Geometricas", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "chilean-lake-district", + "ref": "relax-in-the-termas-geometricas-hot-springs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_relax-in-the-termas-geometricas-hot-springs.jpg" + }, + { + "name": "Go white-water rafting on the Petrohué River", + "description": "Experience an adrenaline-pumping adventure with white-water rafting on the Petrohué River. Navigate through thrilling rapids, surrounded by breathtaking scenery of volcanic landscapes and lush forests. This activity is perfect for adventure seekers looking for an unforgettable experience.", + "locationName": "Petrohué River", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "chilean-lake-district", + "ref": "go-white-water-rafting-on-the-petrohu-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_go-white-water-rafting-on-the-petrohu-river.jpg" + }, + { + "name": "Conquer Volcano Osorno", + "description": "Embark on an exhilarating climb to the summit of the majestic Osorno Volcano. Hike through volcanic landscapes, witness breathtaking panoramic views of the surrounding lakes and Andes Mountains, and feel the thrill of standing atop an active volcano.", + "locationName": "Osorno Volcano", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "chilean-lake-district", + "ref": "conquer-volcano-osorno", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_conquer-volcano-osorno.jpg" + }, + { + "name": "Horseback Riding Adventure", + "description": "Saddle up for an unforgettable horseback riding adventure through the scenic landscapes of the Lake District. Explore hidden trails, traverse rolling hills, and immerse yourself in the region's natural beauty.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "chilean-lake-district", + "ref": "horseback-riding-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_horseback-riding-adventure.jpg" + }, + { + "name": "Sail Away on Lake Todos los Santos", + "description": "Embark on a scenic boat tour across the emerald waters of Lake Todos los Santos. Admire the snow-capped peaks of the Osorno and Puntiagudo volcanoes, discover hidden waterfalls, and enjoy the tranquility of the surrounding nature.", + "locationName": "Lake Todos los Santos", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "chilean-lake-district", + "ref": "sail-away-on-lake-todos-los-santos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_sail-away-on-lake-todos-los-santos.jpg" + }, + { + "name": "Indulge in Chilean Cuisine", + "description": "Embark on a culinary journey through the Chilean Lake District. Sample traditional dishes such as curanto (a seafood and meat stew cooked in a pit), cazuela (a hearty Chilean soup), and empanadas, and savor the flavors of the region.", + "locationName": "Various restaurants", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "chilean-lake-district", + "ref": "indulge-in-chilean-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_indulge-in-chilean-cuisine.jpg" + }, + { + "name": "Stargazing in the Andes", + "description": "Escape the city lights and immerse yourself in the wonders of the night sky. Join a stargazing tour and marvel at the Milky Way, constellations, and celestial objects visible in the clear, unpolluted skies of the Andes Mountains.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "chilean-lake-district", + "ref": "stargazing-in-the-andes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_stargazing-in-the-andes.jpg" + }, + { + "name": "Bike the Cruce Andino", + "description": "Embark on a thrilling cycling adventure through the Andes Mountains, crossing the border between Chile and Argentina. This challenging route offers stunning scenery, including lakes, forests, and snow-capped peaks. You can choose from various tour options, ranging from self-guided to fully supported, depending on your experience and preferences.", + "locationName": "Andes Mountains", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "chilean-lake-district", + "ref": "bike-the-cruce-andino", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_bike-the-cruce-andino.jpg" + }, + { + "name": "Visit Chiloé Island", + "description": "Explore the enchanting Chiloé Island, known for its unique culture, stilt houses, and wooden churches. Discover the island's rich history and folklore, visit the colorful markets, and enjoy fresh seafood. You can also take a boat trip to see penguins and other marine life.", + "locationName": "Chiloé Island", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "chilean-lake-district", + "ref": "visit-chilo-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_visit-chilo-island.jpg" + }, + { + "name": "Go Fly Fishing", + "description": "Cast your line in the pristine rivers and lakes of the Chilean Lake District, renowned for its excellent fly fishing. Experienced guides can help you land brown trout, rainbow trout, and salmon. Enjoy the peaceful surroundings and the thrill of the catch.", + "locationName": "Petrohué River, Lake Llanquihue", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "chilean-lake-district", + "ref": "go-fly-fishing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_go-fly-fishing.jpg" + }, + { + "name": "Skiing and Snowboarding at Antillanca", + "description": "Hit the slopes of Antillanca, a popular ski resort with breathtaking views of Volcano Casablanca. Enjoy a variety of runs for all skill levels, from gentle slopes to challenging black diamonds. The resort also offers snowboarding, snowshoeing, and other winter activities.", + "locationName": "Antillanca Ski Resort", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "chilean-lake-district", + "ref": "skiing-and-snowboarding-at-antillanca", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_skiing-and-snowboarding-at-antillanca.jpg" + }, + { + "name": "Birdwatching in Alerce Andino National Park", + "description": "Discover the diverse birdlife of Alerce Andino National Park, home to numerous species, including the Magellanic woodpecker, chucao tapaculo, and torrent duck. Hike through ancient alerce forests and keep an eye out for these feathered wonders in their natural habitat.", + "locationName": "Alerce Andino National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "chilean-lake-district", + "ref": "birdwatching-in-alerce-andino-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_birdwatching-in-alerce-andino-national-park.jpg" + }, + { + "name": "Scenic Cruise on Lake Todos Los Santos", + "description": "Embark on a breathtaking boat tour across the crystal-clear waters of Lake Todos Los Santos, surrounded by majestic mountains and lush greenery. Capture stunning photographs of the picturesque landscapes, including the iconic Osorno Volcano. Keep an eye out for diverse birdlife and soak in the tranquility of this pristine natural environment.", + "locationName": "Lake Todos Los Santos", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "chilean-lake-district", + "ref": "scenic-cruise-on-lake-todos-los-santos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_scenic-cruise-on-lake-todos-los-santos.jpg" + }, + { + "name": "Visit Petrohué Waterfalls", + "description": "Witness the cascading beauty of Petrohué Waterfalls, where turquoise glacial waters plunge over volcanic rock formations. Take a leisurely walk along the well-maintained trails, enjoying the refreshing mist and the surrounding rainforest scenery. Capture memorable photos of this natural wonder and learn about the geological forces that shaped it.", + "locationName": "Petrohué Waterfalls", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "chilean-lake-district", + "ref": "visit-petrohu-waterfalls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_visit-petrohu-waterfalls.jpg" + }, + { + "name": "Explore the Charming Town of Puerto Varas", + "description": "Wander through the charming streets of Puerto Varas, a lakeside town with German-inspired architecture and a relaxed atmosphere. Discover local shops, indulge in delicious pastries at traditional cafes, and admire the colorful houses along the waterfront. Visit the Sacred Heart of Jesus Church for panoramic views of the lake and Osorno Volcano.", + "locationName": "Puerto Varas", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "chilean-lake-district", + "ref": "explore-the-charming-town-of-puerto-varas", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_explore-the-charming-town-of-puerto-varas.jpg" + }, + { + "name": "Unwind at a Lakeside Lodge", + "description": "Escape to a cozy lakeside lodge and immerse yourself in the tranquility of the Chilean Lake District. Enjoy breathtaking views of the water and mountains from your private balcony, unwind by the fireplace, or indulge in spa treatments. Many lodges offer outdoor activities like kayaking, fishing, or horseback riding.", + "locationName": "Various lakeside locations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "chilean-lake-district", + "ref": "unwind-at-a-lakeside-lodge", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_unwind-at-a-lakeside-lodge.jpg" + }, + { + "name": "Sample Craft Beer at a Local Brewery", + "description": "Discover the burgeoning craft beer scene in the Chilean Lake District. Visit a local brewery and sample a variety of unique and flavorful beers, often made with regional ingredients. Learn about the brewing process, enjoy the friendly atmosphere, and perhaps find your new favorite brew.", + "locationName": "Various towns and cities", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "chilean-lake-district", + "ref": "sample-craft-beer-at-a-local-brewery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/chilean-lake-district_sample-craft-beer-at-a-local-brewery.jpg" + }, + { + "name": "Hike the Sentiero Azzurro", + "description": "Embark on a breathtaking coastal hike along the Sentiero Azzurro, connecting the five villages of Cinque Terre. Enjoy panoramic views of the Ligurian Sea, terraced vineyards, and charming villages. Choose from various trail sections, catering to different fitness levels, and immerse yourself in the beauty of the Italian Riviera.", + "locationName": "Cinque Terre National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "cinque-terre", + "ref": "hike-the-sentiero-azzurro", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_hike-the-sentiero-azzurro.jpg" + }, + { + "name": "Explore the Village of Vernazza", + "description": "Wander through the charming village of Vernazza, with its colorful houses, narrow streets, and picturesque harbor. Visit the Doria Castle for stunning views, relax on the beach, or enjoy a delicious seafood meal at a local trattoria.", + "locationName": "Vernazza", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "cinque-terre", + "ref": "explore-the-village-of-vernazza", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_explore-the-village-of-vernazza.jpg" + }, + { + "name": "Take a Boat Tour", + "description": "Experience the beauty of Cinque Terre from a different perspective with a boat tour. Admire the villages from the sea, explore hidden coves, and swim in crystal-clear waters. Choose from various tour options, including sunset cruises and private boat rentals.", + "locationName": "Cinque Terre coastline", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cinque-terre", + "ref": "take-a-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_take-a-boat-tour.jpg" + }, + { + "name": "Indulge in Local Cuisine", + "description": "Savor the flavors of Ligurian cuisine at one of the many restaurants or trattorias in Cinque Terre. Try fresh seafood dishes, pesto pasta, focaccia bread, and local wines. Don't miss the chance to enjoy a gelato while strolling through the villages.", + "locationName": "Various restaurants and trattorias", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "cinque-terre", + "ref": "indulge-in-local-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_indulge-in-local-cuisine.jpg" + }, + { + "name": "Visit the Cinque Terre Vineyards", + "description": "Discover the local winemaking traditions of Cinque Terre with a visit to a vineyard. Learn about the unique grape varieties grown on the terraced hillsides and enjoy a wine tasting with stunning views of the coastline. Some vineyards also offer tours and food pairings.", + "locationName": "Cinque Terre vineyards", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "cinque-terre", + "ref": "visit-the-cinque-terre-vineyards", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_visit-the-cinque-terre-vineyards.jpg" + }, + { + "name": "Kayaking in the Cinque Terre Marine Protected Area", + "description": "Embark on a sea kayaking adventure to explore the hidden coves, grottos, and marine life of the Cinque Terre Marine Protected Area. Paddle through crystal-clear waters, witness diverse ecosystems, and enjoy breathtaking views of the coastline from a unique perspective.", + "locationName": "Cinque Terre Marine Protected Area", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "cinque-terre", + "ref": "kayaking-in-the-cinque-terre-marine-protected-area", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_kayaking-in-the-cinque-terre-marine-protected-area.jpg" + }, + { + "name": "Swimming and Sunbathing at Monterosso Beach", + "description": "Relax and soak up the sun at Monterosso Beach, the largest and sandiest beach in Cinque Terre. Enjoy a refreshing swim in the turquoise waters, build sandcastles with the kids, or simply unwind on a beach chair with a good book.", + "locationName": "Monterosso Beach", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "cinque-terre", + "ref": "swimming-and-sunbathing-at-monterosso-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_swimming-and-sunbathing-at-monterosso-beach.jpg" + }, + { + "name": "Sunset Watching at Manarola", + "description": "Experience the magic of a Cinque Terre sunset from the charming village of Manarola. Find a scenic spot on the rocks or at a waterfront café and watch the sky transform into a canvas of vibrant colors as the sun dips below the horizon.", + "locationName": "Manarola", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "cinque-terre", + "ref": "sunset-watching-at-manarola", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_sunset-watching-at-manarola.jpg" + }, + { + "name": "Take a Cooking Class", + "description": "Delve into the culinary traditions of Liguria with a hands-on cooking class. Learn to prepare regional specialties like pesto, focaccia, and fresh seafood dishes under the guidance of a local chef. Savor the fruits of your labor with a delicious meal and newfound culinary skills.", + "locationName": "Various locations in Cinque Terre", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "cinque-terre", + "ref": "take-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_take-a-cooking-class.jpg" + }, + { + "name": "Explore the Castle of Riomaggiore", + "description": "Step back in time with a visit to the historic Castle of Riomaggiore. Explore the ruins of this 13th-century fortress, learn about its role in protecting the village from pirates, and enjoy panoramic views of the coastline and surrounding hills.", + "locationName": "Riomaggiore", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cinque-terre", + "ref": "explore-the-castle-of-riomaggiore", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_explore-the-castle-of-riomaggiore.jpg" + }, + { + "name": "Cliff Jumping at Manarola", + "description": "For the adventurous souls, take a thrilling leap from the rocky cliffs into the crystal-clear waters of the Ligurian Sea. Manarola offers several jumping points with varying heights, providing an adrenaline rush and breathtaking views.", + "locationName": "Manarola", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "cinque-terre", + "ref": "cliff-jumping-at-manarola", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_cliff-jumping-at-manarola.jpg" + }, + { + "name": "Explore the Sanctuary of Nostra Signora di Montenero", + "description": "Embark on a spiritual and scenic journey to the Sanctuary of Nostra Signora di Montenero, perched high above Riomaggiore. Enjoy panoramic views of the coastline and surrounding villages while experiencing the tranquility of this religious site.", + "locationName": "Riomaggiore", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cinque-terre", + "ref": "explore-the-sanctuary-of-nostra-signora-di-montenero", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_explore-the-sanctuary-of-nostra-signora-di-montenero.jpg" + }, + { + "name": "Go on a Pesto Making Workshop", + "description": "Delve into the culinary traditions of Liguria with a hands-on pesto making workshop. Learn the secrets of preparing this iconic sauce using fresh, local ingredients, and savor the fruits of your labor with a delicious pasta dish.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "cinque-terre", + "ref": "go-on-a-pesto-making-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_go-on-a-pesto-making-workshop.jpg" + }, + { + "name": "Take a Day Trip to Portovenere", + "description": "Venture beyond the Cinque Terre and explore the charming coastal town of Portovenere. Discover its historic Church of St. Peter, Doria Castle, and Byron's Grotto, and enjoy a leisurely stroll along the picturesque harbor.", + "locationName": "Portovenere", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "cinque-terre", + "ref": "take-a-day-trip-to-portovenere", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_take-a-day-trip-to-portovenere.jpg" + }, + { + "name": "Relax with a Beachside Yoga Session", + "description": "Unwind and reconnect with nature through a rejuvenating yoga session on the beach. Several studios and instructors offer classes with stunning views of the Mediterranean Sea, providing a perfect blend of relaxation and exercise.", + "locationName": "Monterosso al Mare", + "duration": 1.5, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "cinque-terre", + "ref": "relax-with-a-beachside-yoga-session", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_relax-with-a-beachside-yoga-session.jpg" + }, + { + "name": "Scuba Dive the Cinque Terre Coast", + "description": "Embark on an underwater adventure and discover the vibrant marine life of the Cinque Terre National Park. Explore hidden coves, underwater rock formations, and a diverse ecosystem teeming with colorful fish, octopus, and even dolphins. Several dive centers in the area cater to all levels, from beginners to experienced divers, offering guided dives and PADI certification courses.", + "locationName": "Cinque Terre National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "cinque-terre", + "ref": "scuba-dive-the-cinque-terre-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_scuba-dive-the-cinque-terre-coast.jpg" + }, + { + "name": "Take a Romantic Train Ride", + "description": "Experience the charm of the Cinque Terre by train, enjoying breathtaking coastal views from the comfort of your seat. The train connects all five villages, offering a convenient and scenic way to hop between towns. Share a bottle of local wine as you watch the sun set over the Mediterranean Sea, creating a truly unforgettable moment.", + "locationName": "Cinque Terre Train Line", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "cinque-terre", + "ref": "take-a-romantic-train-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_take-a-romantic-train-ride.jpg" + }, + { + "name": "Enjoy a Traditional Ligurian Dinner", + "description": "Immerse yourself in the local culture with an authentic Ligurian dinner at a family-run trattoria. Savor fresh seafood dishes like trofie al pesto, a regional pasta specialty, or Ligurian fish stew. Pair your meal with a glass of Cinque Terre DOC wine for a true taste of the region's culinary delights.", + "locationName": "Local Trattorias in Cinque Terre Villages", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "cinque-terre", + "ref": "enjoy-a-traditional-ligurian-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_enjoy-a-traditional-ligurian-dinner.jpg" + }, + { + "name": "Go Stargazing", + "description": "Escape the village lights and venture to a secluded spot along the coast for a magical stargazing experience. The lack of light pollution in the Cinque Terre offers stunning views of the night sky, allowing you to marvel at the constellations and the Milky Way. Pack a blanket, some snacks, and enjoy a romantic evening under the stars.", + "locationName": "Secluded spots along the Cinque Terre Coast", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "cinque-terre", + "ref": "go-stargazing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_go-stargazing.jpg" + }, + { + "name": "Attend a Local Festival", + "description": "Experience the vibrant cultural traditions of the Cinque Terre by attending one of the many local festivals held throughout the year. From religious celebrations like the Feast of San Lorenzo in Manarola to the Grape Harvest Festival in September, these events offer a glimpse into the region's rich heritage and provide a chance to mingle with locals.", + "locationName": "Various locations throughout Cinque Terre", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "cinque-terre", + "ref": "attend-a-local-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/cinque-terre_attend-a-local-festival.jpg" + }, + { + "name": "Explore the Walled City of Cartagena", + "description": "Step back in time and wander through the enchanting streets of Cartagena's historic Walled City, a UNESCO World Heritage Site. Admire the colorful colonial architecture, visit ancient forts and plazas, and soak up the vibrant atmosphere. Explore the Palace of the Inquisition, shop for local handicrafts at Las Bóvedas, and enjoy a romantic horse-drawn carriage ride at sunset.", + "locationName": "Cartagena", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "colombia", + "ref": "explore-the-walled-city-of-cartagena", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_explore-the-walled-city-of-cartagena.jpg" + }, + { + "name": "Hike in the Cocora Valley", + "description": "Embark on a breathtaking hike through the Cocora Valley, home to the world's tallest palm trees. Marvel at the towering wax palms that reach up to 60 meters tall, surrounded by lush cloud forests and rolling hills. Keep an eye out for unique wildlife, including the Andean condor, and enjoy a picnic lunch amidst the stunning scenery. This hike is suitable for various fitness levels, with options for shorter or longer trails.", + "locationName": "Cocora Valley", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "colombia", + "ref": "hike-in-the-cocora-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_hike-in-the-cocora-valley.jpg" + }, + { + "name": "Discover the Coffee Region", + "description": "Immerse yourself in the world of Colombian coffee with a visit to the Coffee Triangle. Take a tour of a coffee farm, learn about the bean-to-cup process, and participate in a coffee tasting experience. Enjoy the scenic landscapes of rolling hills covered in coffee plantations, and visit charming towns like Salento and Filandia. Learn about the cultural heritage of the region and the importance of coffee to the Colombian economy.", + "locationName": "Coffee Triangle", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "colombia", + "ref": "discover-the-coffee-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_discover-the-coffee-region.jpg" + }, + { + "name": "Experience Medellín's Nightlife", + "description": "Dance the night away in Medellín, known for its vibrant nightlife and energetic atmosphere. Explore the trendy El Poblado neighborhood, where you'll find a variety of bars, clubs, and live music venues. Enjoy salsa dancing, reggaeton beats, or electronic music, and experience the warmth and hospitality of the Paisa people. Don't miss the chance to try Aguardiente, the national liquor of Colombia.", + "locationName": "Medellín", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "colombia", + "ref": "experience-medell-n-s-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_experience-medell-n-s-nightlife.jpg" + }, + { + "name": "Visit the Gold Museum in Bogotá", + "description": "Delve into Colombia's rich history and cultural heritage at the Gold Museum in Bogotá. Discover the largest collection of pre-Columbian gold artifacts in the world, showcasing the craftsmanship and artistry of indigenous cultures. Learn about the symbolism and significance of gold in these societies, and admire the intricate designs and exquisite pieces on display. The museum also offers exhibits on other materials, such as ceramics and textiles, providing a comprehensive overview of Colombian history.", + "locationName": "Bogotá", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "colombia", + "ref": "visit-the-gold-museum-in-bogot-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_visit-the-gold-museum-in-bogot-.jpg" + }, + { + "name": "Sail the Rosario Islands", + "description": "Embark on a boat trip from Cartagena to the Rosario Islands, a stunning archipelago in the Caribbean Sea. Relax on pristine white-sand beaches, snorkel amidst vibrant coral reefs, and enjoy fresh seafood lunches on secluded islands. This is a perfect escape for a day of sun, sea, and serenity.", + "locationName": "Rosario Islands", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "colombia", + "ref": "sail-the-rosario-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_sail-the-rosario-islands.jpg" + }, + { + "name": "Go Whale Watching in the Pacific", + "description": "Witness the awe-inspiring spectacle of humpback whales migrating along the Pacific coast of Colombia. Between June and October, these majestic creatures visit the warm waters to breed and give birth. Join a responsible whale watching tour from Nuquí or Bahía Solano for an unforgettable wildlife encounter.", + "locationName": "Nuquí or Bahía Solano", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "colombia", + "ref": "go-whale-watching-in-the-pacific", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_go-whale-watching-in-the-pacific.jpg" + }, + { + "name": "Explore the Lost City (Ciudad Perdida)", + "description": "Embark on a challenging but rewarding multi-day trek through the jungle to reach the Lost City, an ancient archaeological site built by the Tayrona civilization centuries ago. This adventure involves hiking through diverse ecosystems, crossing rivers, and immersing yourself in the natural beauty and cultural history of the region.", + "locationName": "Sierra Nevada de Santa Marta", + "duration": 48, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "colombia", + "ref": "explore-the-lost-city-ciudad-perdida-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_explore-the-lost-city-ciudad-perdida-.jpg" + }, + { + "name": "Learn to Salsa Dance in Cali", + "description": "Cali, known as the 'Salsa Capital of the World,' is the perfect place to immerse yourself in the vibrant dance culture. Take salsa lessons, join a dance social, or watch professionals showcase their skills at a local club. Whether you're a beginner or an experienced dancer, Cali's infectious energy will have you moving to the rhythm.", + "locationName": "Cali", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "colombia", + "ref": "learn-to-salsa-dance-in-cali", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_learn-to-salsa-dance-in-cali.jpg" + }, + { + "name": "Whitewater Rafting on the Rio Suarez", + "description": "Experience an adrenaline-pumping adventure with a whitewater rafting trip on the Rio Suarez, one of Colombia's most renowned rivers for rafting. Navigate through thrilling rapids surrounded by stunning canyon scenery. This activity is perfect for adventure seekers and nature lovers.", + "locationName": "Rio Suarez", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "colombia", + "ref": "whitewater-rafting-on-the-rio-suarez", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_whitewater-rafting-on-the-rio-suarez.jpg" + }, + { + "name": "Birdwatching in the Amazon Rainforest", + "description": "Embark on an unforgettable journey into the heart of the Amazon Rainforest, a paradise for birdwatchers. With over 1,800 species of birds, including vibrant toucans, majestic harpy eagles, and colorful macaws, you'll be mesmerized by the diversity and beauty of avian life. Join a guided tour with expert naturalists who will lead you through the lush jungle, pointing out hidden birds and sharing their knowledge about the ecosystem.", + "locationName": "Amazon Rainforest", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "colombia", + "ref": "birdwatching-in-the-amazon-rainforest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_birdwatching-in-the-amazon-rainforest.jpg" + }, + { + "name": "Scuba Diving in San Andrés and Providencia", + "description": "Dive into the crystal-clear waters of the Caribbean Sea and discover a vibrant underwater world surrounding the islands of San Andrés and Providencia. Explore colorful coral reefs teeming with marine life, swim alongside tropical fish, and encounter majestic sea turtles. Whether you're a seasoned diver or a beginner, there are dive sites suitable for all levels of experience.", + "locationName": "San Andrés and Providencia", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "colombia", + "ref": "scuba-diving-in-san-andr-s-and-providencia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_scuba-diving-in-san-andr-s-and-providencia.jpg" + }, + { + "name": "Explore the Tatacoa Desert", + "description": "Venture into the otherworldly landscape of the Tatacoa Desert, a unique and awe-inspiring destination in Colombia. Hike through canyons and valleys carved by erosion, marvel at the red and grey rock formations, and visit the observatory for stargazing opportunities under the clear desert sky. The Tatacoa Desert offers a stark contrast to the lush landscapes found elsewhere in Colombia, making it a truly unforgettable experience.", + "locationName": "Tatacoa Desert", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "colombia", + "ref": "explore-the-tatacoa-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_explore-the-tatacoa-desert.jpg" + }, + { + "name": "Visit Guatapé and El Peñol", + "description": "Escape the city and take a day trip to the charming town of Guatapé and the iconic El Peñol rock. Climb the 740 steps to the top of El Peñol for breathtaking panoramic views of the surrounding lakes and islands. Afterward, explore the colorful streets of Guatapé, admire the vibrant houses with their unique zocalos (baseboards), and enjoy a relaxing boat ride on the reservoir.", + "locationName": "Guatapé and El Peñol", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "colombia", + "ref": "visit-guatap-and-el-pe-ol", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_visit-guatap-and-el-pe-ol.jpg" + }, + { + "name": "Relax in the Hot Springs of Santa Rosa de Cabal", + "description": "Indulge in a rejuvenating experience at the natural hot springs of Santa Rosa de Cabal. Immerse yourself in the therapeutic mineral-rich waters, surrounded by lush greenery and breathtaking mountain scenery. Several spa resorts in the area offer a variety of treatments and massages, providing the ultimate relaxation and wellness retreat.", + "locationName": "Santa Rosa de Cabal", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "colombia", + "ref": "relax-in-the-hot-springs-of-santa-rosa-de-cabal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_relax-in-the-hot-springs-of-santa-rosa-de-cabal.jpg" + }, + { + "name": "Paragliding over Medellín", + "description": "Soar above the sprawling cityscape of Medellín and the surrounding Aburrá Valley on a thrilling paragliding adventure. Experience breathtaking panoramic views of the mountains, lush greenery, and the urban landscape below. This activity offers a unique perspective of the city and an adrenaline rush for adventure seekers.", + "locationName": "Medellín", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "colombia", + "ref": "paragliding-over-medell-n", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_paragliding-over-medell-n.jpg" + }, + { + "name": "Explore the Amazon Rainforest", + "description": "Embark on a multi-day expedition into the depths of the Amazon Rainforest, the world's largest tropical rainforest. Discover the incredible biodiversity of the region, including exotic plants, animals, and indigenous communities. Go on jungle treks, boat rides along the Amazon River, and experience the magic of this unique ecosystem.", + "locationName": "Amazon Rainforest", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "colombia", + "ref": "explore-the-amazon-rainforest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_explore-the-amazon-rainforest.jpg" + }, + { + "name": "Visit the Salt Cathedral of Zipaquirá", + "description": "Descend into the depths of a former salt mine and marvel at the architectural wonder of the Salt Cathedral of Zipaquirá. Explore the subterranean tunnels and chambers adorned with intricate salt sculptures and religious iconography. This unique underground cathedral is a testament to human ingenuity and a must-see cultural attraction.", + "locationName": "Zipaquirá", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "colombia", + "ref": "visit-the-salt-cathedral-of-zipaquir-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_visit-the-salt-cathedral-of-zipaquir-.jpg" + }, + { + "name": "Learn to Play Tejo", + "description": "Immerse yourself in Colombian culture by learning to play Tejo, the country's national sport. This traditional game involves throwing metal discs at gunpowder targets, creating small explosions upon impact. Join locals at a Tejo court and experience the friendly competition and festive atmosphere.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "colombia", + "ref": "learn-to-play-tejo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_learn-to-play-tejo.jpg" + }, + { + "name": "Take a Street Food Tour", + "description": "Embark on a culinary adventure through the streets of Colombia's cities and towns. Sample a variety of local delicacies, from empanadas and arepas to exotic fruits and traditional desserts. Learn about the cultural significance of different dishes and experience the vibrant street food scene.", + "locationName": "Various cities and towns", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "colombia", + "ref": "take-a-street-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/colombia_take-a-street-food-tour.jpg" + }, + { + "name": "Hike the GR20 Trail", + "description": "Embark on an epic adventure through Corsica's rugged interior on the renowned GR20 trail. This challenging long-distance hike offers breathtaking views of towering mountains, pristine lakes, and dense forests. Suitable for experienced hikers, the trail is divided into stages, allowing you to customize your itinerary based on your fitness level and time constraints.", + "locationName": "GR20 Trail", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "corsica", + "ref": "hike-the-gr20-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_hike-the-gr20-trail.jpg" + }, + { + "name": "Relax on Palombaggia Beach", + "description": "Unwind on the soft sands of Palombaggia Beach, renowned for its turquoise waters, powdery sand, and stunning views of the Cerbicale Islands. Soak up the Mediterranean sun, swim in the crystal-clear sea, or indulge in water sports such as kayaking and paddleboarding. The beach offers a range of amenities, including beach bars, restaurants, and equipment rentals, ensuring a comfortable and enjoyable experience.", + "locationName": "Palombaggia Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "corsica", + "ref": "relax-on-palombaggia-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_relax-on-palombaggia-beach.jpg" + }, + { + "name": "Explore the Citadel of Bonifacio", + "description": "Step back in time at the historic Citadel of Bonifacio, perched dramatically on white limestone cliffs overlooking the Mediterranean Sea. Wander through the narrow streets, admire the medieval architecture, and visit the Bastion de l'Etendard for panoramic views. Delve into the citadel's rich history at the Bonifacio History Museum, or simply soak up the atmosphere at a charming cafe.", + "locationName": "Citadel of Bonifacio", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "corsica", + "ref": "explore-the-citadel-of-bonifacio", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_explore-the-citadel-of-bonifacio.jpg" + }, + { + "name": "Discover the Scandola Nature Reserve", + "description": "Embark on a boat tour to the Scandola Nature Reserve, a UNESCO World Heritage Site renowned for its dramatic red cliffs, hidden coves, and diverse marine life. Observe rare seabirds, such as the osprey and the Corsican shearwater, and keep an eye out for dolphins playing in the waves. Snorkeling and diving enthusiasts can explore the underwater world, teeming with colorful fish and vibrant coral reefs.", + "locationName": "Scandola Nature Reserve", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "corsica", + "ref": "discover-the-scandola-nature-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_discover-the-scandola-nature-reserve.jpg" + }, + { + "name": "Indulge in Corsican Cuisine", + "description": "Savor the unique flavors of Corsican cuisine, influenced by French and Italian traditions. Sample local specialties such as wild boar stew, chestnut polenta, and brocciu cheese. Pair your meal with a glass of Corsican wine, known for its bold and distinctive character. For a truly immersive experience, visit a traditional agriturismo, where you can enjoy farm-to-table dining amidst the island's scenic landscapes.", + "locationName": "Various restaurants and agriturismos", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "corsica", + "ref": "indulge-in-corsican-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_indulge-in-corsican-cuisine.jpg" + }, + { + "name": "Go Canyoning in the Bavella Needles", + "description": "Embark on an exhilarating canyoning adventure amidst the stunning Bavella Needles. Rappel down waterfalls, slide through natural rock formations, and swim in crystal-clear pools. This activity is perfect for thrill-seekers and nature lovers.", + "locationName": "Bavella Needles", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "corsica", + "ref": "go-canyoning-in-the-bavella-needles", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_go-canyoning-in-the-bavella-needles.jpg" + }, + { + "name": "Sail Along the Coast", + "description": "Experience the beauty of Corsica's coastline from a different perspective with a sailing excursion. Cruise along the turquoise waters, visit hidden coves and beaches, and enjoy breathtaking views of the island's rugged cliffs and charming villages.", + "locationName": "Corsican Coast", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "corsica", + "ref": "sail-along-the-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_sail-along-the-coast.jpg" + }, + { + "name": "Discover Prehistoric Sites", + "description": "Step back in time and explore Corsica's rich history by visiting its fascinating prehistoric sites. Discover ancient megaliths, menhirs, and dolmens, and learn about the island's early inhabitants.", + "locationName": "Filitosa or Filitosa", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "corsica", + "ref": "discover-prehistoric-sites", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_discover-prehistoric-sites.jpg" + }, + { + "name": "Sample Local Wines", + "description": "Indulge in the flavors of Corsica with a wine tasting tour. Visit local vineyards, learn about the island's unique grape varieties, and savor delicious Corsican wines.", + "locationName": "Patrimonio or Ajaccio regions", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "corsica", + "ref": "sample-local-wines", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_sample-local-wines.jpg" + }, + { + "name": "Kayak in the Lavezzi Islands", + "description": "Explore the stunning Lavezzi Islands by kayak. Paddle through crystal-clear waters, discover hidden coves, and enjoy the tranquility of this protected nature reserve.", + "locationName": "Lavezzi Islands", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "corsica", + "ref": "kayak-in-the-lavezzi-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_kayak-in-the-lavezzi-islands.jpg" + }, + { + "name": "Horseback Riding in the Corsican Mountains", + "description": "Embark on a scenic horseback riding adventure through the rugged mountains of Corsica. Traverse ancient trails, breathe in the fresh mountain air, and soak in panoramic views of the island's breathtaking landscapes. This experience is perfect for nature lovers and adventure seekers looking for a unique way to explore the island's interior.", + "locationName": "Corsican Mountains", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "corsica", + "ref": "horseback-riding-in-the-corsican-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_horseback-riding-in-the-corsican-mountains.jpg" + }, + { + "name": "Stargazing in the Remote Villages", + "description": "Escape the city lights and venture into the remote villages of Corsica for an unforgettable stargazing experience. With minimal light pollution, the island's night sky comes alive with a dazzling display of stars and constellations. Join a local astronomy tour or simply find a secluded spot to marvel at the celestial wonders above.", + "locationName": "Remote villages", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "corsica", + "ref": "stargazing-in-the-remote-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_stargazing-in-the-remote-villages.jpg" + }, + { + "name": "Explore the Calanques de Piana by Boat", + "description": "Discover the stunning Calanques de Piana, a series of dramatic red granite cliffs and hidden coves, on a scenic boat tour. Cruise along the turquoise waters, marvel at the towering rock formations, and explore secluded beaches accessible only by water. This is a perfect way to experience the coastal beauty of Corsica from a unique perspective.", + "locationName": "Calanques de Piana", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "corsica", + "ref": "explore-the-calanques-de-piana-by-boat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_explore-the-calanques-de-piana-by-boat.jpg" + }, + { + "name": "Visit the Filitosa Archaeological Site", + "description": "Step back in time at the Filitosa archaeological site, home to mysterious prehistoric statues and megalithic structures dating back thousands of years. Explore the ancient settlements, learn about Corsica's rich history, and marvel at the enigmatic figures carved by early inhabitants of the island.", + "locationName": "Filitosa", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "corsica", + "ref": "visit-the-filitosa-archaeological-site", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_visit-the-filitosa-archaeological-site.jpg" + }, + { + "name": "Attend a Polyphonic Singing Concert", + "description": "Immerse yourself in Corsican culture by attending a traditional polyphonic singing concert. Experience the unique vocal harmonies of this ancient singing style, passed down through generations. These captivating performances offer a glimpse into the island's rich heritage and musical traditions.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "corsica", + "ref": "attend-a-polyphonic-singing-concert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_attend-a-polyphonic-singing-concert.jpg" + }, + { + "name": "Scuba Diving in the Cerbicales Islands", + "description": "Embark on an underwater adventure to the Cerbicales Islands, a protected marine reserve off the southern coast of Corsica. Discover a vibrant world of colorful fish, coral reefs, and ancient shipwrecks, making it a paradise for both beginner and experienced divers.", + "locationName": "Cerbicales Islands", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "corsica", + "ref": "scuba-diving-in-the-cerbicales-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_scuba-diving-in-the-cerbicales-islands.jpg" + }, + { + "name": "Mountain Biking Through the Desert des Agriates", + "description": "Experience the rugged beauty of the Desert des Agriates on a thrilling mountain bike adventure. Navigate through rocky trails, sandy paths, and hidden coves, enjoying breathtaking views of the coastline and the Mediterranean Sea.", + "locationName": "Desert des Agriates", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "corsica", + "ref": "mountain-biking-through-the-desert-des-agriates", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_mountain-biking-through-the-desert-des-agriates.jpg" + }, + { + "name": "Wine Tasting Tour in Patrimonio", + "description": "Indulge in the rich flavors of Corsican wines with a delightful wine tasting tour in the Patrimonio region. Visit charming vineyards, meet passionate winemakers, and savor a variety of local grape varietals, accompanied by stunning vineyard landscapes.", + "locationName": "Patrimonio", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "corsica", + "ref": "wine-tasting-tour-in-patrimonio", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_wine-tasting-tour-in-patrimonio.jpg" + }, + { + "name": "Explore the Cap Corse by Car", + "description": "Embark on a scenic road trip along the Cap Corse, the northernmost peninsula of Corsica. Discover picturesque villages, Genoese towers, hidden beaches, and breathtaking coastal views, stopping at charming cafes and local shops along the way.", + "locationName": "Cap Corse", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "corsica", + "ref": "explore-the-cap-corse-by-car", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_explore-the-cap-corse-by-car.jpg" + }, + { + "name": "Visit the Ajaccio Market", + "description": "Immerse yourself in the vibrant atmosphere of the Ajaccio Market, a bustling hub of local life. Explore stalls overflowing with fresh produce, artisan cheeses, cured meats, and handcrafted souvenirs, experiencing the authentic flavors and culture of Corsica.", + "locationName": "Ajaccio", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "corsica", + "ref": "visit-the-ajaccio-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/corsica_visit-the-ajaccio-market.jpg" + }, + { + "name": "Corcovado National Park Hike", + "description": "Embark on a guided trek through the heart of Corcovado National Park, known as one of the most biodiverse places on Earth. Encounter an abundance of wildlife, including monkeys, sloths, tapirs, scarlet macaws, and perhaps even the elusive jaguar.", + "locationName": "Corcovado National Park", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "costa-rica", + "ref": "corcovado-national-park-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_corcovado-national-park-hike.jpg" + }, + { + "name": "Kayaking in Drake Bay", + "description": "Paddle through the calm waters of Drake Bay, surrounded by lush mangroves and rainforest-covered islands. Keep an eye out for dolphins, sea turtles, and a variety of colorful fish. This is a perfect activity for a relaxing afternoon immersed in nature.", + "locationName": "Drake Bay", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "costa-rica", + "ref": "kayaking-in-drake-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_kayaking-in-drake-bay.jpg" + }, + { + "name": "Nighttime Wildlife Tour", + "description": "Venture into the rainforest at night, when nocturnal creatures come alive. With the help of a guide and a flashlight, spot fascinating animals like kinkajous, olingos, frogs, and owls. This unique experience offers a glimpse into the hidden world of the jungle after dark.", + "locationName": "Various locations throughout the peninsula", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "costa-rica", + "ref": "nighttime-wildlife-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_nighttime-wildlife-tour.jpg" + }, + { + "name": "Surfing at Matapalo", + "description": "Catch some waves at Matapalo, a secluded beach known for its consistent surf breaks. Whether you're a beginner or an experienced surfer, you'll find suitable waves to enjoy the thrill of riding the Pacific Ocean.", + "locationName": "Matapalo Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "costa-rica", + "ref": "surfing-at-matapalo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_surfing-at-matapalo.jpg" + }, + { + "name": "Birdwatching Excursion", + "description": "Join a guided birdwatching tour to discover the incredible avian diversity of the Osa Peninsula. With over 450 bird species recorded in the area, you'll have the opportunity to see toucans, hummingbirds, parrots, and many other colorful and exotic birds.", + "locationName": "Various locations throughout the peninsula", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "costa-rica", + "ref": "birdwatching-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_birdwatching-excursion.jpg" + }, + { + "name": "Horseback Riding Adventure", + "description": "Embark on a scenic horseback riding tour through the lush rainforests and pristine beaches of the Osa Peninsula. Traverse jungle trails, ford rivers, and discover hidden waterfalls, all while enjoying the company of these gentle creatures. This is a unique way to experience the beauty and tranquility of the Osa Peninsula.", + "locationName": "Various locations throughout the peninsula", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "costa-rica", + "ref": "horseback-riding-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_horseback-riding-adventure.jpg" + }, + { + "name": "Dolphin and Whale Watching Tour", + "description": "Set sail on a thrilling boat tour in search of majestic marine life. The Osa Peninsula's waters are home to dolphins, whales, and other fascinating creatures. Watch in awe as they breach and play in their natural habitat, creating unforgettable memories.", + "locationName": "Drake Bay or Golfo Dulce", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "costa-rica", + "ref": "dolphin-and-whale-watching-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_dolphin-and-whale-watching-tour.jpg" + }, + { + "name": "Scuba Diving or Snorkeling in Caño Island Biological Reserve", + "description": "Explore the vibrant underwater world of Caño Island Biological Reserve, a renowned diving and snorkeling destination. Discover colorful coral reefs teeming with tropical fish, sea turtles, and other marine life. Whether you're a seasoned diver or a beginner snorkeler, this is an experience not to be missed.", + "locationName": "Caño Island Biological Reserve", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "costa-rica", + "ref": "scuba-diving-or-snorkeling-in-ca-o-island-biological-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_scuba-diving-or-snorkeling-in-ca-o-island-biological-reserve.jpg" + }, + { + "name": "Indigenous Cultural Experience", + "description": "Immerse yourself in the rich culture and traditions of the indigenous communities that call the Osa Peninsula home. Visit a local village, learn about their way of life, and participate in traditional activities such as handicraft making or cooking classes. This is a meaningful way to connect with the local people and gain a deeper understanding of their heritage.", + "locationName": "Indigenous villages within the peninsula", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "costa-rica", + "ref": "indigenous-cultural-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_indigenous-cultural-experience.jpg" + }, + { + "name": "Sunset Cruise with Bioluminescence", + "description": "Embark on a magical sunset cruise along the coast of the Osa Peninsula. As the sun dips below the horizon, witness the breathtaking colors of the sky and the ocean. As darkness falls, be amazed by the bioluminescent plankton illuminating the water with their ethereal glow. This is a truly unforgettable experience.", + "locationName": "Drake Bay or Golfo Dulce", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "costa-rica", + "ref": "sunset-cruise-with-bioluminescence", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_sunset-cruise-with-bioluminescence.jpg" + }, + { + "name": "Mangrove Kayaking Tour", + "description": "Explore the intricate network of mangrove forests in the Sierpe River wetlands by kayak. Glide through the calm waters, observe the fascinating ecosystem, and encounter various bird species, reptiles, and even crocodiles in their natural habitat.", + "locationName": "Sierpe River", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "costa-rica", + "ref": "mangrove-kayaking-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_mangrove-kayaking-tour.jpg" + }, + { + "name": "Waterfall Hike and Swim", + "description": "Embark on a refreshing hike through the rainforest to discover hidden waterfalls cascading into crystal-clear pools. Take a dip in the cool water, surrounded by lush vegetation and the sounds of nature.", + "locationName": "Various locations throughout the Osa Peninsula", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "costa-rica", + "ref": "waterfall-hike-and-swim", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_waterfall-hike-and-swim.jpg" + }, + { + "name": "Night Walk in the Rainforest", + "description": "Experience the magic of the rainforest after dark with a guided night walk. Witness the nocturnal creatures come alive, listen to the symphony of insects, and marvel at the bioluminescent fungi that illuminate the forest floor.", + "locationName": "Various locations throughout the Osa Peninsula", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "costa-rica", + "ref": "night-walk-in-the-rainforest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_night-walk-in-the-rainforest.jpg" + }, + { + "name": "Sustainable Chocolate Farm Tour", + "description": "Delve into the world of chocolate making with a visit to a sustainable cacao farm. Learn about the bean-to-bar process, from harvesting to production, and indulge in delicious chocolate tastings.", + "locationName": "Various locations throughout the Osa Peninsula", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "costa-rica", + "ref": "sustainable-chocolate-farm-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_sustainable-chocolate-farm-tour.jpg" + }, + { + "name": "Relaxation at a Jungle Spa", + "description": "Unwind and rejuvenate with a spa treatment amidst the tranquility of the rainforest. Choose from a variety of massages, body wraps, and other wellness therapies inspired by local ingredients and traditions.", + "locationName": "Various eco-lodges and resorts", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "costa-rica", + "ref": "relaxation-at-a-jungle-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_relaxation-at-a-jungle-spa.jpg" + }, + { + "name": "Sport Fishing Adventure", + "description": "Embark on an exhilarating sport fishing expedition in the Pacific Ocean waters surrounding the Osa Peninsula. Experienced local guides will lead you to the best fishing spots, where you can try your hand at catching marlin, sailfish, tuna, and other prized game fish. Whether you're a seasoned angler or a beginner, this adventure promises excitement and the chance to reel in a trophy catch.", + "locationName": "Pacific Ocean", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "costa-rica", + "ref": "sport-fishing-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_sport-fishing-adventure.jpg" + }, + { + "name": "Canopy Zipline Tour", + "description": "Soar through the rainforest canopy on an exhilarating zipline adventure. Experience the thrill of flying between platforms high above the jungle floor, enjoying breathtaking views of the surrounding landscape. This activity offers a unique perspective of the Osa Peninsula's biodiversity and is perfect for adrenaline seekers.", + "locationName": "Rainforest canopy", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "costa-rica", + "ref": "canopy-zipline-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_canopy-zipline-tour.jpg" + }, + { + "name": "Stargazing Experience", + "description": "Escape the city lights and immerse yourself in the wonders of the night sky. Join a guided stargazing tour led by an expert astronomer who will unveil the secrets of the cosmos. Learn about constellations, planets, and galaxies while marveling at the brilliance of the Milky Way.", + "locationName": "Remote location away from light pollution", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "costa-rica", + "ref": "stargazing-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_stargazing-experience.jpg" + }, + { + "name": "Culinary Journey: Traditional Costa Rican Cooking Class", + "description": "Delve into the vibrant flavors of Costa Rican cuisine by participating in a hands-on cooking class. Learn the secrets of preparing authentic dishes like gallo pinto, casado, and ceviche under the guidance of a local chef. This immersive experience will tantalize your taste buds and provide you with culinary skills to recreate these delicious recipes back home.", + "locationName": "Local cooking school or restaurant", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "costa-rica", + "ref": "culinary-journey-traditional-costa-rican-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_culinary-journey-traditional-costa-rican-cooking-class.jpg" + }, + { + "name": "Off-Road ATV Excursion", + "description": "Embark on an adrenaline-pumping off-road adventure through the rugged terrain of the Osa Peninsula. Navigate muddy trails, cross rivers, and conquer challenging hills on a thrilling ATV ride. This exhilarating experience will take you deep into the jungle, offering a unique perspective of the peninsula's diverse landscapes.", + "locationName": "Off-road trails", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "costa-rica", + "ref": "off-road-atv-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/costa-rica_off-road-atv-excursion.jpg" + }, + { + "name": "Ascend the Burj Khalifa", + "description": "Experience breathtaking panoramic views of Dubai's skyline from the observation deck of the Burj Khalifa, the world's tallest building. Marvel at the city's iconic landmarks, including the Palm Jumeirah and the Dubai Fountain, from a unique perspective.", + "locationName": "Burj Khalifa", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "dubai", + "ref": "ascend-the-burj-khalifa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_ascend-the-burj-khalifa.jpg" + }, + { + "name": "Explore the Dubai Mall", + "description": "Indulge in a shopping extravaganza at the Dubai Mall, one of the world's largest shopping destinations. Discover a vast array of luxury brands, high-street fashion, and unique boutiques, along with an indoor theme park, an ice rink, and a stunning aquarium.", + "locationName": "Dubai Mall", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubai", + "ref": "explore-the-dubai-mall", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_explore-the-dubai-mall.jpg" + }, + { + "name": "Embark on a Desert Safari", + "description": "Escape the city and venture into the golden sands of the Arabian Desert on an exhilarating desert safari. Experience dune bashing, sandboarding, camel riding, and a traditional Bedouin camp with cultural performances and a delicious barbecue dinner.", + "locationName": "Dubai Desert Conservation Reserve", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubai", + "ref": "embark-on-a-desert-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_embark-on-a-desert-safari.jpg" + }, + { + "name": "Discover the Dubai Fountain Show", + "description": "Witness the captivating spectacle of the Dubai Fountain Show, a mesmerizing display of water, music, and light. Set on the Burj Khalifa Lake, the fountain dances to a variety of melodies, creating a truly unforgettable experience.", + "locationName": "Burj Khalifa Lake", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "dubai", + "ref": "discover-the-dubai-fountain-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_discover-the-dubai-fountain-show.jpg" + }, + { + "name": "Visit the Jumeirah Mosque", + "description": "Immerse yourself in Islamic culture with a visit to the Jumeirah Mosque, a stunning example of modern Islamic architecture. Take a guided tour to learn about the mosque's history and significance, and admire its intricate details and serene atmosphere.", + "locationName": "Jumeirah Mosque", + "duration": 1, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubai", + "ref": "visit-the-jumeirah-mosque", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_visit-the-jumeirah-mosque.jpg" + }, + { + "name": "Hot Air Balloon Ride over the Desert", + "description": "Experience the breathtaking beauty of the Dubai desert from a unique perspective with a hot air balloon ride. Ascend at sunrise for panoramic views of the golden dunes, watch falcons soar through the sky, and enjoy a sense of tranquility as you float above the landscape. This unforgettable experience is perfect for capturing stunning photos and creating lasting memories.", + "locationName": "Dubai Desert Conservation Reserve", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "dubai", + "ref": "hot-air-balloon-ride-over-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_hot-air-balloon-ride-over-the-desert.jpg" + }, + { + "name": "Explore the Al Fahidi Historical Neighbourhood", + "description": "Step back in time and immerse yourself in Dubai's rich history at the Al Fahidi Historical Neighbourhood. Wander through the narrow alleyways, admire the traditional wind towers, and visit the Dubai Museum to learn about the city's evolution. This cultural experience offers a glimpse into Dubai's past and provides a stark contrast to its modern skyscrapers.", + "locationName": "Al Fahidi Historical Neighbourhood", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubai", + "ref": "explore-the-al-fahidi-historical-neighbourhood", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_explore-the-al-fahidi-historical-neighbourhood.jpg" + }, + { + "name": "Indulge in a Relaxing Spa Day", + "description": "Escape the hustle and bustle of the city and rejuvenate your mind and body with a luxurious spa day. Dubai offers a wide range of world-class spas, each with unique treatments and amenities. Indulge in a traditional hammam experience, enjoy a massage, or simply unwind in a serene environment. This is the perfect way to de-stress and pamper yourself during your vacation.", + "locationName": "Various spas throughout Dubai", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "dubai", + "ref": "indulge-in-a-relaxing-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_indulge-in-a-relaxing-spa-day.jpg" + }, + { + "name": "Cruise along Dubai Creek on a Traditional Dhow", + "description": "Experience the charm of old Dubai with a traditional dhow cruise along Dubai Creek. Admire the city's skyline from the water, enjoy a delicious dinner on board, and soak up the vibrant atmosphere. This is a great way to see a different side of Dubai and learn about its maritime heritage.", + "locationName": "Dubai Creek", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubai", + "ref": "cruise-along-dubai-creek-on-a-traditional-dhow", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_cruise-along-dubai-creek-on-a-traditional-dhow.jpg" + }, + { + "name": "Visit the Dubai Miracle Garden", + "description": "Immerse yourself in a world of floral wonder at the Dubai Miracle Garden. Marvel at the intricate displays of millions of flowers, arranged in stunning sculptures and patterns. This unique attraction is a feast for the senses and a perfect spot for photography enthusiasts.", + "locationName": "Dubai Miracle Garden", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubai", + "ref": "visit-the-dubai-miracle-garden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_visit-the-dubai-miracle-garden.jpg" + }, + { + "name": "Kite Beach", + "description": "Soak up the sun and enjoy the vibrant atmosphere of Kite Beach. This popular spot offers stunning views of the Burj Al Arab and a wide range of activities such as swimming, sunbathing, kite surfing, and beach volleyball. The beach also features a variety of cafes and restaurants, making it the perfect place to spend a relaxing day by the sea.", + "locationName": "Kite Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubai", + "ref": "kite-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_kite-beach.jpg" + }, + { + "name": "Ski Dubai", + "description": "Experience the thrill of skiing and snowboarding in the heart of the desert at Ski Dubai, an indoor ski resort located within the Mall of the Emirates. With five slopes of varying difficulty, a snow park, and even penguin encounters, Ski Dubai offers a unique and unforgettable winter wonderland experience.", + "locationName": "Mall of the Emirates", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubai", + "ref": "ski-dubai", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_ski-dubai.jpg" + }, + { + "name": "Dubai Opera", + "description": "Immerse yourself in the world of performing arts at the iconic Dubai Opera. This architectural masterpiece hosts a diverse range of shows, including opera, ballet, theatre, and concerts. Enjoy a memorable evening of entertainment and culture in a truly stunning setting.", + "locationName": "Dubai Opera", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "dubai", + "ref": "dubai-opera", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_dubai-opera.jpg" + }, + { + "name": "IMG Worlds of Adventure", + "description": "Embark on an exhilarating adventure at IMG Worlds of Adventure, one of the largest indoor theme parks in the world. With four themed zones featuring rides, attractions, and experiences based on popular characters and franchises, IMG Worlds of Adventure offers endless fun for all ages.", + "locationName": "IMG Worlds of Adventure", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "dubai", + "ref": "img-worlds-of-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_img-worlds-of-adventure.jpg" + }, + { + "name": "Dubai Frame", + "description": "Capture breathtaking panoramic views of Dubai from the Dubai Frame, a unique architectural landmark that resembles a giant picture frame. Experience the contrast between Old and New Dubai as you gaze out at the city's iconic skyline and historic neighborhoods from a 150-meter-high sky deck.", + "locationName": "Zabeel Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubai", + "ref": "dubai-frame", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_dubai-frame.jpg" + }, + { + "name": "Jet Ski Adventure on the Arabian Gulf", + "description": "Experience the thrill of gliding across the turquoise waters of the Arabian Gulf on a jet ski. Feel the wind in your hair as you zoom past iconic landmarks like the Burj Al Arab and Palm Jumeirah. Whether you're a seasoned rider or a first-timer, this exhilarating adventure is sure to get your adrenaline pumping.", + "locationName": "Jumeirah Beach Residence (JBR) Beach", + "duration": 1.5, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "dubai", + "ref": "jet-ski-adventure-on-the-arabian-gulf", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_jet-ski-adventure-on-the-arabian-gulf.jpg" + }, + { + "name": "Immerse Yourself in Art and Culture at Alserkal Avenue", + "description": "Escape the glitz and glamour and delve into Dubai's thriving art scene at Alserkal Avenue. Explore contemporary art galleries showcasing works by local and international artists, attend thought-provoking exhibitions, and discover hidden gems in this vibrant cultural hub. With its trendy cafes and unique shops, Alserkal Avenue offers a refreshing and inspiring experience.", + "locationName": "Al Quoz Industrial Area", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubai", + "ref": "immerse-yourself-in-art-and-culture-at-alserkal-avenue", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_immerse-yourself-in-art-and-culture-at-alserkal-avenue.jpg" + }, + { + "name": "Savor Authentic Emirati Cuisine at Al Fanar Restaurant", + "description": "Embark on a culinary journey through Emirati heritage at Al Fanar Restaurant. Step into a traditional setting and indulge in authentic dishes like machboos, thereed, and luqaimat. With its warm hospitality and rich flavors, Al Fanar offers a cultural and gastronomic experience that will transport you to the heart of Emirati traditions.", + "locationName": "Al Seef", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubai", + "ref": "savor-authentic-emirati-cuisine-at-al-fanar-restaurant", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_savor-authentic-emirati-cuisine-at-al-fanar-restaurant.jpg" + }, + { + "name": "Witness the Magic of La Perle", + "description": "Prepare to be mesmerized by La Perle, a breathtaking aqua-based show that combines acrobatics, aquatic stunts, and stunning visuals. Watch in awe as talented performers dive, fly, and dance through the air, creating a spectacle that will leave you speechless.", + "locationName": "Al Habtoor City", + "duration": 1.5, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "dubai", + "ref": "witness-the-magic-of-la-perle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_witness-the-magic-of-la-perle.jpg" + }, + { + "name": "Shop for Unique Souvenirs at the Spice Souk and Gold Souk", + "description": "Immerse yourself in the vibrant atmosphere of Dubai's traditional markets. Wander through the aromatic Spice Souk, where you can find exotic spices, herbs, and teas from around the world. Then, head to the dazzling Gold Souk, where you can marvel at exquisite gold jewelry and handcrafted pieces.", + "locationName": "Deira", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubai", + "ref": "shop-for-unique-souvenirs-at-the-spice-souk-and-gold-souk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubai_shop-for-unique-souvenirs-at-the-spice-souk-and-gold-souk.jpg" + }, + { + "name": "Walk the Ancient City Walls", + "description": "Embark on a journey through time as you walk along the magnificent city walls of Dubrovnik, a UNESCO World Heritage Site. Take in breathtaking panoramic views of the Adriatic Sea, the terracotta rooftops of the Old Town, and the surrounding islands. Explore historical forts and towers, and immerse yourself in the rich history of this ancient city.", + "locationName": "Dubrovnik City Walls", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubrovnik", + "ref": "walk-the-ancient-city-walls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_walk-the-ancient-city-walls.jpg" + }, + { + "name": "Explore the Old Town", + "description": "Step into a labyrinth of narrow cobblestone streets and discover the charm of Dubrovnik's Old Town. Admire the stunning architecture, from the Renaissance Rector's Palace to the Baroque St. Blaise Church. Visit the Franciscan Monastery with its tranquil cloister and ancient pharmacy, or explore the Sponza Palace, a masterpiece of Gothic and Renaissance styles. Don't forget to enjoy a coffee or a delicious meal at one of the many cafes and restaurants.", + "locationName": "Dubrovnik Old Town", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubrovnik", + "ref": "explore-the-old-town", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_explore-the-old-town.jpg" + }, + { + "name": "Kayaking and Snorkeling Adventure", + "description": "Embark on a sea kayaking adventure along the Dubrovnik coastline. Paddle through crystal-clear waters, explore hidden caves and secluded beaches, and discover the underwater world with snorkeling gear. This is a fantastic way to experience the natural beauty of the Adriatic Sea while enjoying some physical activity.", + "locationName": "Dubrovnik Coastline", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubrovnik", + "ref": "kayaking-and-snorkeling-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_kayaking-and-snorkeling-adventure.jpg" + }, + { + "name": "Game of Thrones Tour", + "description": "Calling all Game of Thrones fans! Immerse yourselves in the world of Westeros with a guided tour of Dubrovnik's iconic filming locations. Relive memorable scenes, learn behind-the-scenes secrets, and discover why Dubrovnik was chosen as the perfect setting for King's Landing.", + "locationName": "Various locations in Dubrovnik", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubrovnik", + "ref": "game-of-thrones-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_game-of-thrones-tour.jpg" + }, + { + "name": "Cable Car Ride to Mount Srđ", + "description": "Take a scenic cable car ride to the top of Mount Srđ and enjoy breathtaking panoramic views of Dubrovnik and the surrounding islands. Visit the Imperial Fort, a historical landmark offering stunning vistas, and explore the Homeland War Museum to learn about the city's recent history. For a romantic experience, enjoy a memorable sunset dinner at the Panorama Restaurant.", + "locationName": "Mount Srđ", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubrovnik", + "ref": "cable-car-ride-to-mount-sr-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_cable-car-ride-to-mount-sr-.jpg" + }, + { + "name": "Island Hopping Adventure", + "description": "Embark on a boat tour to the Elafiti Islands, a cluster of idyllic islands just off the coast of Dubrovnik. Explore hidden coves, relax on pristine beaches, and enjoy swimming in the crystal-clear waters. Some tours offer lunch stops at local restaurants on the islands, allowing you to savor fresh seafood and traditional Croatian cuisine.", + "locationName": "Elafiti Islands", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubrovnik", + "ref": "island-hopping-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_island-hopping-adventure.jpg" + }, + { + "name": "Sea Kayaking along the Coast", + "description": "Experience Dubrovnik's stunning coastline from a different perspective with a sea kayaking adventure. Paddle along the city walls, explore hidden caves, and admire the panoramic views of the Adriatic Sea. Guided tours often include snorkeling stops, allowing you to discover the underwater world teeming with marine life.", + "locationName": "Dubrovnik Coast", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubrovnik", + "ref": "sea-kayaking-along-the-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_sea-kayaking-along-the-coast.jpg" + }, + { + "name": "Lokrum Island Escape", + "description": "Take a short ferry ride to Lokrum Island, a natural oasis just a stone's throw from Dubrovnik's Old Town. Explore the botanical gardens, visit the Benedictine monastery ruins, and discover the legendary \"Dead Sea\", a small saltwater lake. Enjoy a picnic amidst the peaceful surroundings or simply relax on the rocky beaches.", + "locationName": "Lokrum Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubrovnik", + "ref": "lokrum-island-escape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_lokrum-island-escape.jpg" + }, + { + "name": "Culinary Delights at Konoba Dubrava", + "description": "Indulge in an authentic Croatian dining experience at Konoba Dubrava, a charming restaurant nestled in the hills above Dubrovnik. Savor traditional dishes like peka (slow-cooked meat and vegetables), fresh seafood specialties, and homemade desserts. Enjoy breathtaking views of the city and the Adriatic Sea while relishing the flavors of the region.", + "locationName": "Konoba Dubrava", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubrovnik", + "ref": "culinary-delights-at-konoba-dubrava", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_culinary-delights-at-konoba-dubrava.jpg" + }, + { + "name": "Sunset Cruise with Dinner", + "description": "Embark on a romantic sunset cruise along the Dubrovnik coastline. Sail past the city walls and nearby islands, enjoying breathtaking views of the Adriatic as the sun dips below the horizon. Many cruises offer delicious dinner options, allowing you to savor local cuisine while admiring the picturesque scenery.", + "locationName": "Dubrovnik Coast", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "dubrovnik", + "ref": "sunset-cruise-with-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_sunset-cruise-with-dinner.jpg" + }, + { + "name": "Wine Tasting Tour in the Konavle Valley", + "description": "Embark on a delightful journey through the picturesque Konavle Valley, renowned for its fertile vineyards and family-run wineries. Indulge in the region's rich winemaking traditions as you sample exquisite local wines, from robust reds to crisp whites, paired with delectable regional specialties. Immerse yourself in the stunning landscapes and charming villages, learning about the unique grape varieties and winemaking techniques that make Konavle wines so special.", + "locationName": "Konavle Valley", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "dubrovnik", + "ref": "wine-tasting-tour-in-the-konavle-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_wine-tasting-tour-in-the-konavle-valley.jpg" + }, + { + "name": "Hiking and Swimming at Mount Srđ", + "description": "For breathtaking panoramic views of Dubrovnik and the Adriatic Sea, embark on a hike up Mount Srđ. Follow scenic trails through lush Mediterranean vegetation, reaching the summit where you'll be rewarded with stunning vistas. Cool off with a refreshing swim at the hidden beach below, or explore the historic Imperial Fortress, a testament to Dubrovnik's resilience.", + "locationName": "Mount Srđ", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubrovnik", + "ref": "hiking-and-swimming-at-mount-sr-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_hiking-and-swimming-at-mount-sr-.jpg" + }, + { + "name": "Explore the Elafiti Islands by Boat", + "description": "Escape the bustling city and set sail on a boat trip to the Elafiti Islands, a cluster of idyllic islands just off the coast of Dubrovnik. Discover hidden coves, swim in crystal-clear waters, and explore charming fishing villages. Visit Koločep, Šipan, and Lopud, each offering unique landscapes, historical sites, and relaxed island vibes.", + "locationName": "Elafiti Islands", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubrovnik", + "ref": "explore-the-elafiti-islands-by-boat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_explore-the-elafiti-islands-by-boat.jpg" + }, + { + "name": "Catch a Performance at the Dubrovnik Summer Festival", + "description": "Immerse yourself in the vibrant cultural scene of Dubrovnik by attending a performance at the renowned Dubrovnik Summer Festival. Held annually from July to August, the festival showcases a diverse program of theatre, music, dance, and opera performances at various historical venues throughout the city. Experience the magic of open-air performances under the starry sky, surrounded by Dubrovnik's enchanting architecture.", + "locationName": "Various locations in Dubrovnik Old Town", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "dubrovnik", + "ref": "catch-a-performance-at-the-dubrovnik-summer-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_catch-a-performance-at-the-dubrovnik-summer-festival.jpg" + }, + { + "name": "Discover Local Life at Gruž Market", + "description": "Immerse yourself in the local culture and experience the sights, sounds, and aromas of Dubrovnik's Gruž Market. Browse through stalls overflowing with fresh produce, local cheeses, cured meats, and handmade crafts. Engage with friendly vendors, sample regional delicacies, and find unique souvenirs to remember your trip.", + "locationName": "Gruž Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "dubrovnik", + "ref": "discover-local-life-at-gru-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_discover-local-life-at-gru-market.jpg" + }, + { + "name": "Cliff Jumping Adventure", + "description": "For thrill-seekers, experience the adrenaline rush of cliff jumping into the crystal-clear Adriatic Sea. Local guides will lead you to hidden coves and cliffs, ensuring safety while providing an unforgettable adventure.", + "locationName": "Various coastal locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "dubrovnik", + "ref": "cliff-jumping-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_cliff-jumping-adventure.jpg" + }, + { + "name": "Relaxing Beach Day at Banje Beach", + "description": "Soak up the sun and unwind on the picturesque Banje Beach, located just outside the Old Town walls. Rent a sun lounger, enjoy refreshing cocktails from nearby bars, and take a dip in the azure waters for a perfect beach escape.", + "locationName": "Banje Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubrovnik", + "ref": "relaxing-beach-day-at-banje-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_relaxing-beach-day-at-banje-beach.jpg" + }, + { + "name": "Museum Hopping in Dubrovnik", + "description": "Delve into Dubrovnik's rich history and culture by visiting its diverse museums. Explore the Rector's Palace, the Maritime Museum, or the Franciscan Monastery Museum to discover fascinating artifacts and stories.", + "locationName": "Various museums in Dubrovnik", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "dubrovnik", + "ref": "museum-hopping-in-dubrovnik", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_museum-hopping-in-dubrovnik.jpg" + }, + { + "name": "Sea Kayaking and Snorkeling at Betina Cave", + "description": "Embark on a sea kayaking adventure to the enchanting Betina Cave, accessible only by water. Explore the cave's interior, snorkel in the turquoise waters, and marvel at the natural beauty of the hidden gem.", + "locationName": "Betina Cave", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "dubrovnik", + "ref": "sea-kayaking-and-snorkeling-at-betina-cave", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_sea-kayaking-and-snorkeling-at-betina-cave.jpg" + }, + { + "name": "Scenic Hike to Fort Lovrijenac", + "description": "Enjoy a scenic hike to Fort Lovrijenac, a historic fortress perched on a cliff overlooking the Adriatic Sea. Capture breathtaking panoramic views of the city and coastline while immersing yourself in the fort's fascinating history.", + "locationName": "Fort Lovrijenac", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "dubrovnik", + "ref": "scenic-hike-to-fort-lovrijenac", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/dubrovnik_scenic-hike-to-fort-lovrijenac.jpg" + }, + { + "name": "Explore the Rock-Hewn Churches of Lalibela", + "description": "Embark on a spiritual journey to Lalibela, a UNESCO World Heritage Site, and marvel at the 11 monolithic churches carved directly into the rock. Witness the intricate architecture, learn about their religious significance, and experience the unique atmosphere of this ancient pilgrimage site.", + "locationName": "Lalibela", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "ethiopia", + "ref": "explore-the-rock-hewn-churches-of-lalibela", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_explore-the-rock-hewn-churches-of-lalibela.jpg" + }, + { + "name": "Trekking in the Simien Mountains", + "description": "Embark on an unforgettable trekking adventure through the breathtaking Simien Mountains National Park. Hike amidst dramatic landscapes, encounter endemic wildlife such as the Gelada baboons, and enjoy panoramic views from Ras Dashen, Ethiopia's highest peak.", + "locationName": "Simien Mountains National Park", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "ethiopia", + "ref": "trekking-in-the-simien-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_trekking-in-the-simien-mountains.jpg" + }, + { + "name": "Discover the Vibrant Culture of the Omo Valley", + "description": "Immerse yourself in the rich cultural tapestry of the Omo Valley, home to diverse indigenous tribes like the Mursi, Hamar, and Karo. Witness their unique customs, adornments, and traditional way of life, and gain insights into their fascinating cultures.", + "locationName": "Omo Valley", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "ethiopia", + "ref": "discover-the-vibrant-culture-of-the-omo-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_discover-the-vibrant-culture-of-the-omo-valley.jpg" + }, + { + "name": "Visit the Bustling Merkato Market in Addis Ababa", + "description": "Experience the vibrant energy of Merkato, one of the largest open-air markets in Africa. Explore a labyrinth of stalls selling everything from spices and textiles to handicrafts and electronics, and get a taste of local life in Addis Ababa.", + "locationName": "Addis Ababa", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ethiopia", + "ref": "visit-the-bustling-merkato-market-in-addis-ababa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_visit-the-bustling-merkato-market-in-addis-ababa.jpg" + }, + { + "name": "Coffee Ceremony and Cultural Experience", + "description": "Participate in a traditional Ethiopian coffee ceremony, a social ritual that involves roasting, grinding, and brewing coffee beans. Learn about the cultural significance of coffee in Ethiopia and enjoy the unique flavors and aromas of this ancient tradition.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "ethiopia", + "ref": "coffee-ceremony-and-cultural-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_coffee-ceremony-and-cultural-experience.jpg" + }, + { + "name": "Visit the Danakil Depression", + "description": "Embark on an adventurous expedition to the Danakil Depression, one of the hottest and lowest places on Earth. Witness the surreal landscapes of salt flats, sulfur springs, and volcanic formations. Explore the unique geology and encounter the Afar people, known for their resilience and distinctive culture.", + "locationName": "Danakil Depression", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "ethiopia", + "ref": "visit-the-danakil-depression", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_visit-the-danakil-depression.jpg" + }, + { + "name": "Explore the Blue Nile Falls", + "description": "Witness the breathtaking beauty of the Blue Nile Falls, also known as Tis Abay. Hike through lush landscapes to reach the cascading waterfalls and feel the mist on your face. Capture stunning photos and enjoy the serene atmosphere of this natural wonder.", + "locationName": "Blue Nile Falls", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "ethiopia", + "ref": "explore-the-blue-nile-falls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_explore-the-blue-nile-falls.jpg" + }, + { + "name": "Discover the Ancient City of Aksum", + "description": "Step back in time and explore the ancient city of Aksum, a UNESCO World Heritage Site. Marvel at the towering stelae, obelisks, and ruins of palaces and temples. Learn about the rich history and legends of the Aksumite Empire, one of the most powerful civilizations in ancient Africa.", + "locationName": "Aksum", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "ethiopia", + "ref": "discover-the-ancient-city-of-aksum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_discover-the-ancient-city-of-aksum.jpg" + }, + { + "name": "Relax at Lake Tana", + "description": "Escape to the tranquil shores of Lake Tana, the largest lake in Ethiopia. Take a boat trip to visit the island monasteries, known for their beautiful frescoes and historical significance. Enjoy swimming, birdwatching, or simply relaxing by the serene waters.", + "locationName": "Lake Tana", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ethiopia", + "ref": "relax-at-lake-tana", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_relax-at-lake-tana.jpg" + }, + { + "name": "Wildlife Watching in Awash National Park", + "description": "Embark on a wildlife adventure in Awash National Park, home to diverse animal species. Spot lions, zebras, gazelles, and various bird species in their natural habitat. Enjoy game drives, guided walks, and the chance to experience the Ethiopian wilderness.", + "locationName": "Awash National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "ethiopia", + "ref": "wildlife-watching-in-awash-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_wildlife-watching-in-awash-national-park.jpg" + }, + { + "name": "Birdwatching in the Bale Mountains", + "description": "Embark on a captivating birdwatching adventure in the Bale Mountains National Park, home to diverse avian species including the endemic Ethiopian wolf, mountain nyala, and numerous birds of prey. Hike through stunning landscapes, spot colorful birds, and immerse yourself in the tranquility of nature.", + "locationName": "Bale Mountains National Park", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "ethiopia", + "ref": "birdwatching-in-the-bale-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_birdwatching-in-the-bale-mountains.jpg" + }, + { + "name": "Visit the Harar Jugol, the Fortified Historic Town", + "description": "Step back in time and explore the ancient walled city of Harar Jugol, a UNESCO World Heritage Site. Wander through narrow alleyways, admire the traditional Harari houses, visit vibrant markets, and witness the unique Hyena feeding ritual.", + "locationName": "Harar", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ethiopia", + "ref": "visit-the-harar-jugol-the-fortified-historic-town", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_visit-the-harar-jugol-the-fortified-historic-town.jpg" + }, + { + "name": "White-Water Rafting on the Omo River", + "description": "Experience an adrenaline-pumping adventure with white-water rafting on the Omo River. Navigate through thrilling rapids, witness stunning scenery, and encounter remote villages along the riverbanks.", + "locationName": "Omo River", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "ethiopia", + "ref": "white-water-rafting-on-the-omo-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_white-water-rafting-on-the-omo-river.jpg" + }, + { + "name": "Sample Ethiopian Cuisine and Honey Wine", + "description": "Embark on a culinary journey and savor the unique flavors of Ethiopian cuisine. Try traditional dishes like injera (flatbread), wat (stews), and kitfo (minced beef), and indulge in the sweet taste of tej, a local honey wine.", + "locationName": "Various restaurants and cafes throughout Ethiopia", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "ethiopia", + "ref": "sample-ethiopian-cuisine-and-honey-wine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_sample-ethiopian-cuisine-and-honey-wine.jpg" + }, + { + "name": "Attend the Timkat Festival (Ethiopian Epiphany)", + "description": "Immerse yourself in the vibrant cultural celebration of Timkat, the Ethiopian Epiphany. Witness colorful processions, traditional dances, and the reenactment of Jesus' baptism, a truly unforgettable experience.", + "locationName": "Various locations throughout Ethiopia", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "ethiopia", + "ref": "attend-the-timkat-festival-ethiopian-epiphany-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_attend-the-timkat-festival-ethiopian-epiphany-.jpg" + }, + { + "name": "Horseback Riding in the Gheralta Mountains", + "description": "Embark on a scenic horseback riding adventure through the Gheralta Mountains, known for their dramatic landscapes and rock-hewn churches. Explore hidden trails, visit remote villages, and enjoy breathtaking views of the surrounding valleys. This activity is suitable for riders of all levels and offers a unique perspective on the region's natural beauty and cultural heritage.", + "locationName": "Gheralta Mountains", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "ethiopia", + "ref": "horseback-riding-in-the-gheralta-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_horseback-riding-in-the-gheralta-mountains.jpg" + }, + { + "name": "Hot Air Balloon Ride over the Rift Valley", + "description": "Soar above the stunning landscapes of the Ethiopian Rift Valley in a hot air balloon. Witness panoramic views of volcanic craters, shimmering lakes, and traditional villages as you drift peacefully through the sky. This unforgettable experience offers a unique perspective on the region's geological wonders and cultural diversity.", + "locationName": "Ethiopian Rift Valley", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "ethiopia", + "ref": "hot-air-balloon-ride-over-the-rift-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_hot-air-balloon-ride-over-the-rift-valley.jpg" + }, + { + "name": "Visit the Yirgacheffe Coffee Farms", + "description": "Delve into the world of Ethiopian coffee with a visit to the renowned Yirgacheffe coffee farms. Learn about the coffee cultivation process, from bean to cup, and participate in a traditional coffee ceremony. Savor the rich flavors of freshly brewed Yirgacheffe coffee and discover why Ethiopia is considered the birthplace of coffee.", + "locationName": "Yirgacheffe", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ethiopia", + "ref": "visit-the-yirgacheffe-coffee-farms", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_visit-the-yirgacheffe-coffee-farms.jpg" + }, + { + "name": "Explore the Ruins of Gondar", + "description": "Step back in time at the Royal Enclosure of Gondar, a UNESCO World Heritage Site. Explore the impressive castles, palaces, and churches built by Ethiopian emperors during the 17th and 18th centuries. Discover the rich history and architectural marvels of this once-powerful kingdom.", + "locationName": "Gondar", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ethiopia", + "ref": "explore-the-ruins-of-gondar", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_explore-the-ruins-of-gondar.jpg" + }, + { + "name": "Stargazing in the Danakil Depression", + "description": "Experience the magic of the night sky in the Danakil Depression, one of the lowest and hottest places on Earth. Away from light pollution, marvel at the brilliance of the Milky Way and constellations. This unique experience offers a chance to connect with nature and appreciate the vastness of the universe.", + "locationName": "Danakil Depression", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "ethiopia", + "ref": "stargazing-in-the-danakil-depression", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ethiopia_stargazing-in-the-danakil-depression.jpg" + }, + { + "name": "Explore the Etruscan and Roman Ruins", + "description": "Step back in time and discover the ancient history of Fiesole at the Archaeological Area. Explore the remains of Etruscan walls, Roman baths, and an ancient theatre, imagining life in this hilltop town centuries ago. The panoramic views of the surrounding Tuscan countryside add to the magic of this historical experience.", + "locationName": "Archaeological Area of Fiesole", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiesole", + "ref": "explore-the-etruscan-and-roman-ruins", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_explore-the-etruscan-and-roman-ruins.jpg" + }, + { + "name": "Visit the Fiesole Cathedral", + "description": "Immerse yourself in the spiritual and artistic beauty of the Cattedrale di San Romolo. Admire the Romanesque architecture, intricate frescoes, and serene atmosphere of this historic cathedral. Take a moment for quiet reflection and appreciate the cultural significance of this religious landmark.", + "locationName": "Cattedrale di San Romolo", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "fiesole", + "ref": "visit-the-fiesole-cathedral", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_visit-the-fiesole-cathedral.jpg" + }, + { + "name": "Stroll through the Historic Center", + "description": "Wander through the charming streets of Fiesole's historic center, lined with quaint shops, cafes, and centuries-old buildings. Discover hidden squares, admire the local architecture, and soak up the authentic Italian atmosphere. This leisurely walk is a perfect way to experience the town's unique character and charm.", + "locationName": "Fiesole Historic Center", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "fiesole", + "ref": "stroll-through-the-historic-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_stroll-through-the-historic-center.jpg" + }, + { + "name": "Enjoy Panoramic Views at the Monastery of San Francesco", + "description": "Hike or drive up to the Monastery of San Francesco, perched on the highest point of Fiesole. Be rewarded with breathtaking panoramic views of Florence, the Arno Valley, and the rolling Tuscan hills. Explore the peaceful monastery grounds and enjoy a moment of serenity surrounded by nature's beauty.", + "locationName": "Monastery of San Francesco", + "duration": 1.5, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "fiesole", + "ref": "enjoy-panoramic-views-at-the-monastery-of-san-francesco", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_enjoy-panoramic-views-at-the-monastery-of-san-francesco.jpg" + }, + { + "name": "Indulge in Tuscan Cuisine", + "description": "Treat your taste buds to the delicious flavors of Tuscan cuisine at one of Fiesole's charming restaurants or trattorias. Savor local specialties like ribollita (vegetable soup), bistecca alla Fiorentina (Florentine steak), and cantucci (almond biscotti) paired with a glass of Chianti wine. Enjoy a romantic dinner with stunning views or a casual lunch in a cozy atmosphere.", + "locationName": "Various restaurants in Fiesole", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiesole", + "ref": "indulge-in-tuscan-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_indulge-in-tuscan-cuisine.jpg" + }, + { + "name": "Hike to the top of Monte Ceceri", + "description": "Embark on a scenic hike to the summit of Monte Ceceri, where you'll be rewarded with panoramic views of Florence and the surrounding Tuscan landscape. This moderate hike takes you through picturesque olive groves and offers a glimpse into the region's natural beauty.", + "locationName": "Monte Ceceri", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiesole", + "ref": "hike-to-the-top-of-monte-ceceri", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_hike-to-the-top-of-monte-ceceri.jpg" + }, + { + "name": "Discover the Archaeological Area", + "description": "Delve into Fiesole's rich history at the Archaeological Area, home to Etruscan and Roman ruins, including an ancient theatre, baths, and temples. Explore the remnants of these ancient civilizations and imagine life centuries ago.", + "locationName": "Archaeological Area of Fiesole", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiesole", + "ref": "discover-the-archaeological-area", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_discover-the-archaeological-area.jpg" + }, + { + "name": "Visit the Bandini Museum", + "description": "Immerse yourself in art and history at the Bandini Museum, housed in a 14th-century palace. Admire its collection of Renaissance paintings and sculptures, including works by Della Robbia and Lorenzo Monaco. The museum also offers stunning views of the Tuscan countryside.", + "locationName": "Bandini Museum", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "fiesole", + "ref": "visit-the-bandini-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_visit-the-bandini-museum.jpg" + }, + { + "name": "Enjoy a Romantic Evening at a Local Trattoria", + "description": "Savor a delightful Tuscan dinner at a charming trattoria in Fiesole. Indulge in regional specialties like bistecca alla Fiorentina or pasta with wild boar ragu, paired with a glass of local Chianti wine. The intimate atmosphere and breathtaking views create a perfect setting for a romantic evening.", + "locationName": "Various trattorias in Fiesole", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "fiesole", + "ref": "enjoy-a-romantic-evening-at-a-local-trattoria", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_enjoy-a-romantic-evening-at-a-local-trattoria.jpg" + }, + { + "name": "Take a Cooking Class", + "description": "Learn the secrets of Tuscan cuisine by taking a cooking class. Discover the art of making fresh pasta, regional sauces, and traditional dishes under the guidance of a local chef. This hands-on experience allows you to bring a taste of Italy back home.", + "locationName": "Various cooking schools in Fiesole", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "fiesole", + "ref": "take-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_take-a-cooking-class.jpg" + }, + { + "name": "Truffle Hunting Experience", + "description": "Embark on a sensory adventure with a guided truffle hunt in the picturesque Tuscan countryside. Led by expert truffle hunters and their trained dogs, you'll discover the secrets of finding these prized delicacies. Learn about the different types of truffles, their growth cycle, and the traditional methods used to locate them. After the hunt, savor the unique flavors of fresh truffles with a delicious lunch or dinner at a local farmhouse.", + "locationName": "Tuscan Countryside near Fiesole", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "fiesole", + "ref": "truffle-hunting-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_truffle-hunting-experience.jpg" + }, + { + "name": "Wine Tasting at a Local Vineyard", + "description": "Indulge in the rich flavors of Tuscan wines with a visit to a nearby vineyard. Explore the vineyards, learn about the winemaking process, and sample a variety of local wines, including Chianti Classico, Brunello di Montalcino, and Vernaccia di San Gimignano. Many vineyards also offer stunning views of the rolling hills and charming villages, making it a perfect afternoon escape.", + "locationName": "Vineyards surrounding Fiesole", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "fiesole", + "ref": "wine-tasting-at-a-local-vineyard", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_wine-tasting-at-a-local-vineyard.jpg" + }, + { + "name": "Bike Tour through the Tuscan Hills", + "description": "Experience the beauty of the Tuscan landscape at your own pace with a leisurely bike tour. Rent a bike and follow scenic routes through olive groves, vineyards, and charming villages. Enjoy the fresh air, breathtaking views, and the opportunity to stop at local farms and wineries for refreshments and tastings.", + "locationName": "Tuscan Hills surrounding Fiesole", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiesole", + "ref": "bike-tour-through-the-tuscan-hills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_bike-tour-through-the-tuscan-hills.jpg" + }, + { + "name": "Sunset Picnic with Panoramic Views", + "description": "Create a romantic and unforgettable experience with a sunset picnic overlooking the stunning Tuscan panorama. Pack a basket with local delicacies, find a secluded spot with breathtaking views, and enjoy a peaceful evening as the sun sets over the rolling hills and the city of Florence in the distance.", + "locationName": "Hills surrounding Fiesole with panoramic views", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "fiesole", + "ref": "sunset-picnic-with-panoramic-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_sunset-picnic-with-panoramic-views.jpg" + }, + { + "name": "Attend a Classical Music Concert", + "description": "Immerse yourself in the enchanting world of classical music with a concert at a historic venue in Fiesole. Enjoy performances by talented musicians in a beautiful setting, such as the Roman Theatre or the Fiesole Cathedral. The intimate atmosphere and acoustics of these venues create a truly special and memorable experience.", + "locationName": "Roman Theatre or Fiesole Cathedral", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiesole", + "ref": "attend-a-classical-music-concert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_attend-a-classical-music-concert.jpg" + }, + { + "name": "Olive Oil Tasting and Farm Tour", + "description": "Immerse yourself in the world of Tuscan olive oil with a visit to a local olive farm. Learn about the cultivation and production process, from grove to bottle, and savor the distinct flavors of freshly pressed extra virgin olive oil. Many farms offer tours that include a stroll through the olive groves, a demonstration of traditional pressing techniques, and a tasting session paired with local delicacies.", + "locationName": "Local olive farms in the Fiesole countryside", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiesole", + "ref": "olive-oil-tasting-and-farm-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_olive-oil-tasting-and-farm-tour.jpg" + }, + { + "name": "Explore the Bardini Garden", + "description": "Escape the crowds and discover the enchanting Bardini Garden, a hidden gem with breathtaking views of Florence. Wander through terraced gardens adorned with vibrant flowers, sculptures, and fountains. Enjoy a peaceful picnic amidst the greenery and capture stunning photos of the panoramic vistas.", + "locationName": "Bardini Garden", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiesole", + "ref": "explore-the-bardini-garden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_explore-the-bardini-garden.jpg" + }, + { + "name": "Visit the San Domenico Monastery", + "description": "Delve into the spiritual heart of Fiesole with a visit to the San Domenico Monastery. Explore the serene cloisters, admire Renaissance masterpieces by Fra Angelico, and learn about the monastery's rich history. The peaceful atmosphere and stunning views make it a perfect place for quiet reflection.", + "locationName": "San Domenico Monastery", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiesole", + "ref": "visit-the-san-domenico-monastery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_visit-the-san-domenico-monastery.jpg" + }, + { + "name": "Take a Pottery Workshop", + "description": "Unleash your creativity and learn the art of Tuscan pottery. Join a workshop led by local artisans and try your hand at shaping clay on a potter's wheel. Create your own unique piece of ceramics to take home as a special souvenir of your trip.", + "locationName": "Local pottery studios in Fiesole", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "fiesole", + "ref": "take-a-pottery-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_take-a-pottery-workshop.jpg" + }, + { + "name": "Stargazing Experience", + "description": "Escape the city lights and embark on a magical stargazing experience. Join a guided tour led by astronomy enthusiasts and learn about constellations, planets, and the wonders of the night sky. With minimal light pollution, Fiesole offers a perfect setting for observing the cosmos.", + "locationName": "Open areas with clear views of the sky, such as the top of Monte Ceceri", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiesole", + "ref": "stargazing-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiesole_stargazing-experience.jpg" + }, + { + "name": "Scuba Diving Adventure in the Soft Coral Capital", + "description": "Embark on an underwater adventure to explore the world-renowned coral reefs of Fiji, often called the 'Soft Coral Capital'. Dive into a kaleidoscope of vibrant colors as you encounter diverse marine life, from playful clownfish to majestic manta rays. Whether you're a seasoned diver or a beginner, Fiji offers dive sites suitable for all levels of experience. Discover the magic beneath the waves and create unforgettable memories in this underwater paradise.", + "locationName": "Rainbow Reef, Somosomo Strait", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "fiji", + "ref": "scuba-diving-adventure-in-the-soft-coral-capital", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_scuba-diving-adventure-in-the-soft-coral-capital.jpg" + }, + { + "name": "Island Hopping and Snorkeling", + "description": "Experience the beauty of Fiji's scattered islands with a leisurely island-hopping tour. Cruise through turquoise waters, stopping at idyllic islands where you can relax on pristine beaches, swim in crystal-clear lagoons, and explore vibrant coral reefs while snorkeling. Each island offers its own unique charm and a chance to immerse yourself in the local culture and way of life.", + "locationName": "Mamanuca Islands or Yasawa Islands", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiji", + "ref": "island-hopping-and-snorkeling", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_island-hopping-and-snorkeling.jpg" + }, + { + "name": "Sunset Cruise with Traditional Fijian Feast", + "description": "Sail into the golden sunset on a traditional Fijian outrigger canoe or a modern catamaran. As the sky transforms into a canvas of vibrant colors, savor a delicious Fijian feast prepared with fresh local ingredients. Enjoy the warm hospitality of the Fijian people and be captivated by their enchanting cultural performances, creating an unforgettable evening.", + "locationName": "Denarau Marina or Port Denarau", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiji", + "ref": "sunset-cruise-with-traditional-fijian-feast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_sunset-cruise-with-traditional-fijian-feast.jpg" + }, + { + "name": "Kava Ceremony and Village Visit", + "description": "Immerse yourself in Fijian culture with a visit to a traditional village. Participate in a kava ceremony, a significant cultural ritual where you'll share a bowl of kava, a mildly narcotic drink made from the root of the pepper plant. Learn about Fijian customs, traditions, and the unique way of life in these remote communities.", + "locationName": "Navala Village or Viseisei Village", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiji", + "ref": "kava-ceremony-and-village-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_kava-ceremony-and-village-visit.jpg" + }, + { + "name": "Relaxation and Rejuvenation at a Fijian Spa", + "description": "Indulge in a pampering spa experience amidst the tranquility of Fiji's tropical paradise. Choose from a variety of treatments inspired by traditional Fijian practices and natural ingredients. Let your worries melt away as skilled therapists soothe your body and mind, leaving you feeling refreshed and rejuvenated.", + "locationName": "Various resorts and spas throughout Fiji", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "fiji", + "ref": "relaxation-and-rejuvenation-at-a-fijian-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_relaxation-and-rejuvenation-at-a-fijian-spa.jpg" + }, + { + "name": "Sigatoka River Safari", + "description": "Embark on an exhilarating jet boat ride up the Sigatoka River, Fiji's longest river. Journey through the lush rainforest, passing traditional Fijian villages and witnessing the captivating beauty of the island's interior. This adventure offers a unique blend of cultural immersion and thrilling excitement.", + "locationName": "Sigatoka River", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiji", + "ref": "sigatoka-river-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_sigatoka-river-safari.jpg" + }, + { + "name": "Garden of the Sleeping Giant", + "description": "Immerse yourself in the tranquil beauty of the Garden of the Sleeping Giant, a horticultural paradise nestled at the foothills of the Nausori Highlands. Stroll through vibrant orchid collections, admire diverse native flora, and discover the captivating legends surrounding this enchanting garden. Perfect for nature enthusiasts and those seeking a peaceful escape.", + "locationName": "Nausori Highlands", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiji", + "ref": "garden-of-the-sleeping-giant", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_garden-of-the-sleeping-giant.jpg" + }, + { + "name": "Cloud 9 Floating Bar", + "description": "Experience the ultimate in relaxation and revelry at Cloud 9, a unique floating bar located in the turquoise waters of the Mamanuca Islands. Soak up the sun, sip on tropical cocktails, enjoy delicious wood-fired pizzas, and dance to the rhythm of the ocean waves. This adults-only haven promises an unforgettable day of fun and indulgence.", + "locationName": "Mamanuca Islands", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "fiji", + "ref": "cloud-9-floating-bar", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_cloud-9-floating-bar.jpg" + }, + { + "name": "Navala Village Visit", + "description": "Step back in time with a visit to Navala Village, a traditional Fijian settlement nestled amidst the Ba Highlands. Immerse yourself in the rich cultural heritage of the villagers, witness their unique way of life, and gain insights into their customs and traditions. This authentic experience offers a glimpse into the heart and soul of Fiji.", + "locationName": "Ba Highlands", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiji", + "ref": "navala-village-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_navala-village-visit.jpg" + }, + { + "name": "Fiji Museum", + "description": "Delve into Fiji's captivating history and cultural heritage at the Fiji Museum, located in the heart of Suva. Explore fascinating exhibits showcasing archaeological artifacts, traditional crafts, and historical photographs. Gain a deeper understanding of the island nation's past, from its early settlers to its colonial era and beyond.", + "locationName": "Suva", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "fiji", + "ref": "fiji-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_fiji-museum.jpg" + }, + { + "name": "Hiking the Lavena Coastal Walk", + "description": "Embark on a scenic coastal hike along the Lavena Coastal Walk on Taveuni Island. This moderately challenging trail winds through lush rainforests, offering breathtaking views of the coastline, secluded beaches, and cascading waterfalls. Keep an eye out for unique Fijian wildlife and immerse yourself in the island's natural beauty.", + "locationName": "Taveuni Island", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiji", + "ref": "hiking-the-lavena-coastal-walk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_hiking-the-lavena-coastal-walk.jpg" + }, + { + "name": "Exploring the Kula Wild Adventure Park", + "description": "Get up close and personal with Fiji's native wildlife at the Kula Wild Adventure Park. This eco-friendly park offers a range of activities, including ziplining through the rainforest canopy, encountering iguanas and parrots, and learning about conservation efforts. It's an educational and exciting experience for all ages.", + "locationName": "Kula Wild Adventure Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiji", + "ref": "exploring-the-kula-wild-adventure-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_exploring-the-kula-wild-adventure-park.jpg" + }, + { + "name": "Indulging in a Traditional Lovo Feast", + "description": "Experience the authentic flavors of Fiji with a traditional Lovo feast. This unique cooking method involves burying food in an underground oven lined with hot stones, resulting in tender meats, flavorful vegetables, and delicious root crops. Enjoy this communal dining experience while learning about Fijian culture and customs.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiji", + "ref": "indulging-in-a-traditional-lovo-feast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_indulging-in-a-traditional-lovo-feast.jpg" + }, + { + "name": "Kayaking and Stand-Up Paddleboarding Adventures", + "description": "Explore the crystal-clear waters of Fiji at your own pace with kayaking and stand-up paddleboarding adventures. Glide along the coastline, discover hidden coves, and marvel at the vibrant marine life below. Whether you're a beginner or an experienced paddler, there are options for all skill levels.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiji", + "ref": "kayaking-and-stand-up-paddleboarding-adventures", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_kayaking-and-stand-up-paddleboarding-adventures.jpg" + }, + { + "name": "Shopping for Fijian Handicrafts and Souvenirs", + "description": "Discover unique Fijian handicrafts and souvenirs at local markets and shops. Browse through a variety of items, including hand-woven baskets, wood carvings, tapa cloth, and pearl jewelry. This is a great opportunity to support local artisans and find meaningful gifts to remember your Fijian adventure.", + "locationName": "Nadi, Suva, and other towns", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiji", + "ref": "shopping-for-fijian-handicrafts-and-souvenirs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_shopping-for-fijian-handicrafts-and-souvenirs.jpg" + }, + { + "name": "Jet Boat Adventure on the Sigatoka River", + "description": "Embark on a thrilling jet boat ride through the heart of Fiji's lush rainforest, experiencing high-speed turns, spins, and splashes as you navigate the Sigatoka River. Witness stunning natural scenery, including cascading waterfalls and towering volcanic peaks, while learning about the region's rich history and culture from your knowledgeable guide.", + "locationName": "Sigatoka River", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "fiji", + "ref": "jet-boat-adventure-on-the-sigatoka-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_jet-boat-adventure-on-the-sigatoka-river.jpg" + }, + { + "name": "Traditional Fijian Cooking Class", + "description": "Immerse yourself in Fijian culture by participating in a hands-on cooking class, where you'll learn to prepare authentic dishes using fresh local ingredients. Discover the secrets of traditional cooking methods, such as using an underground oven or 'lovo', and savor the delicious flavors of Fiji's culinary heritage.", + "locationName": "Varies - local villages or resorts", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "fiji", + "ref": "traditional-fijian-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_traditional-fijian-cooking-class.jpg" + }, + { + "name": "Firewalking Ceremony at Beqa Island", + "description": "Witness the ancient Fijian tradition of firewalking, a mesmerizing cultural performance unique to Beqa Island. Experience the spiritual significance and history behind this ritual, as skilled firewalkers demonstrate their courage and faith by walking barefoot across burning hot stones.", + "locationName": "Beqa Island", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "fiji", + "ref": "firewalking-ceremony-at-beqa-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_firewalking-ceremony-at-beqa-island.jpg" + }, + { + "name": "Stargazing and Night Kayaking Tour", + "description": "Embark on a magical night kayaking adventure under the starlit Fijian sky. Paddle through calm waters, surrounded by the tranquil sounds of nature, and marvel at the brilliance of the Milky Way. Learn about Polynesian navigation techniques and hear captivating legends about the constellations.", + "locationName": "Varies - coastal areas", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "fiji", + "ref": "stargazing-and-night-kayaking-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_stargazing-and-night-kayaking-tour.jpg" + }, + { + "name": "Horseback Riding through the Highlands", + "description": "Explore the scenic beauty of Fiji's highlands on horseback, venturing through lush rainforests, open meadows, and traditional villages. Enjoy breathtaking panoramic views, encounter friendly locals, and experience the authentic charm of Fijian rural life.", + "locationName": "Viti Levu or Vanua Levu highlands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "fiji", + "ref": "horseback-riding-through-the-highlands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/fiji_horseback-riding-through-the-highlands.jpg" + }, + { + "name": "Skiing in Chamonix", + "description": "Experience the thrill of skiing in Chamonix, a world-renowned resort nestled at the foot of Mont Blanc. With diverse slopes for all levels, from gentle beginner runs to challenging black diamonds, Chamonix offers an unforgettable skiing adventure. Enjoy breathtaking views of the snow-capped peaks and indulge in the après-ski scene in the charming village.", + "locationName": "Chamonix", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "french-alps", + "ref": "skiing-in-chamonix", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_skiing-in-chamonix.jpg" + }, + { + "name": "Hiking in the Aiguilles Rouges Nature Reserve", + "description": "Embark on a scenic hike through the Aiguilles Rouges Nature Reserve, a protected area boasting stunning alpine landscapes. Discover diverse flora and fauna, cascading waterfalls, and panoramic views of the Mont Blanc massif. Choose from various trails catering to different fitness levels, allowing you to immerse yourself in the natural beauty of the French Alps.", + "locationName": "Aiguilles Rouges Nature Reserve", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-alps", + "ref": "hiking-in-the-aiguilles-rouges-nature-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_hiking-in-the-aiguilles-rouges-nature-reserve.jpg" + }, + { + "name": "Paragliding over the Chamonix Valley", + "description": "Soar through the skies on a thrilling paragliding experience, taking in breathtaking aerial views of the Chamonix Valley. Accompanied by a certified instructor, feel the adrenaline rush as you glide over snow-capped peaks, lush forests, and charming villages. This unforgettable adventure offers a unique perspective of the majestic French Alps.", + "locationName": "Chamonix Valley", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-alps", + "ref": "paragliding-over-the-chamonix-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_paragliding-over-the-chamonix-valley.jpg" + }, + { + "name": "Exploring the charming village of Annecy", + "description": "Wander through the picturesque canals and cobbled streets of Annecy, known as the \"Venice of the Alps.\" Discover its rich history, admire the colorful houses lining the waterways, and visit the iconic Palais de l'Isle. Enjoy a leisurely boat ride on Lake Annecy or explore the vibrant local markets for unique souvenirs.", + "locationName": "Annecy", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-alps", + "ref": "exploring-the-charming-village-of-annecy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_exploring-the-charming-village-of-annecy.jpg" + }, + { + "name": "Indulging in Savoyard cuisine", + "description": "Treat your taste buds to the delectable flavors of Savoyard cuisine. Savor local specialties such as fondue, raclette, and tartiflette, made with regional cheeses and fresh ingredients. Pair your meal with a glass of Savoy wine and enjoy the cozy ambiance of a traditional mountain restaurant.", + "locationName": "Various restaurants throughout the French Alps", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-alps", + "ref": "indulging-in-savoyard-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_indulging-in-savoyard-cuisine.jpg" + }, + { + "name": "White-Water Rafting on the Durance River", + "description": "Experience the thrill of navigating the rapids on the Durance River, surrounded by breathtaking alpine scenery. This exhilarating activity is perfect for adventure seekers and nature enthusiasts alike.", + "locationName": "Durance River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-alps", + "ref": "white-water-rafting-on-the-durance-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_white-water-rafting-on-the-durance-river.jpg" + }, + { + "name": "Scenic Train Ride on the Mont Blanc Express", + "description": "Embark on a picturesque journey through the heart of the Alps aboard the Mont Blanc Express. Marvel at the towering peaks, charming villages, and lush valleys as you traverse this historic railway.", + "locationName": "Mont Blanc Express", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-alps", + "ref": "scenic-train-ride-on-the-mont-blanc-express", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_scenic-train-ride-on-the-mont-blanc-express.jpg" + }, + { + "name": "Relaxing Spa Day at a Thermal Bath", + "description": "Indulge in a rejuvenating spa experience at one of the region's renowned thermal baths. Unwind in the soothing mineral-rich waters, enjoy a massage, and let the stress melt away.", + "locationName": "Various thermal baths throughout the French Alps", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-alps", + "ref": "relaxing-spa-day-at-a-thermal-bath", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_relaxing-spa-day-at-a-thermal-bath.jpg" + }, + { + "name": "Visit the Historic Fort de Tamié", + "description": "Step back in time at the Fort de Tamié, a 19th-century fortress perched high above the Albertville Valley. Explore the fortifications, learn about its military history, and enjoy panoramic views of the surrounding mountains.", + "locationName": "Fort de Tamié", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-alps", + "ref": "visit-the-historic-fort-de-tami-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_visit-the-historic-fort-de-tami-.jpg" + }, + { + "name": "Sample Local Cheeses at a Traditional Market", + "description": "Immerse yourself in the local culture by visiting a traditional market. Discover a variety of artisanal cheeses, fresh produce, and regional specialties while soaking up the lively atmosphere.", + "locationName": "Various markets throughout the French Alps", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "french-alps", + "ref": "sample-local-cheeses-at-a-traditional-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_sample-local-cheeses-at-a-traditional-market.jpg" + }, + { + "name": "Mountain Biking", + "description": "Explore the diverse terrain of the French Alps on two wheels! Numerous trails cater to all skill levels, from gentle paths through valleys to challenging mountain climbs. Rent a bike and embark on an adventure through picturesque landscapes, enjoying breathtaking views and fresh mountain air.", + "locationName": "Various locations throughout the French Alps", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-alps", + "ref": "mountain-biking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_mountain-biking.jpg" + }, + { + "name": "Visit the Mer de Glace", + "description": "Embark on a journey to the Mer de Glace, the largest glacier in France! Take a scenic train ride on the Montenvers Railway and descend into the ice cave to witness the mesmerizing blue hues and learn about the glacier's history and formation.", + "locationName": "Chamonix", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-alps", + "ref": "visit-the-mer-de-glace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_visit-the-mer-de-glace.jpg" + }, + { + "name": "Stargazing in the Mountains", + "description": "Escape the city lights and experience the magic of the night sky in the French Alps. Join a stargazing tour or simply find a secluded spot away from light pollution. With minimal interference, marvel at the Milky Way, constellations, and shooting stars for an unforgettable evening.", + "locationName": "Various locations throughout the French Alps", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "french-alps", + "ref": "stargazing-in-the-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_stargazing-in-the-mountains.jpg" + }, + { + "name": "Canyoning Adventure", + "description": "Get your adrenaline pumping with a canyoning experience! Descend through stunning canyons, rappel down waterfalls, slide down natural water slides, and jump into refreshing pools. This exhilarating activity combines adventure, nature, and stunning scenery.", + "locationName": "Various locations throughout the French Alps", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-alps", + "ref": "canyoning-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_canyoning-adventure.jpg" + }, + { + "name": "Discover Local Crafts and Traditions", + "description": "Immerse yourself in the rich cultural heritage of the French Alps by exploring local crafts and traditions. Visit workshops and studios to observe artisans creating wood carvings, ceramics, and textiles. Learn about the history and significance of these crafts and perhaps even try your hand at creating your own masterpiece.", + "locationName": "Various villages throughout the French Alps", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-alps", + "ref": "discover-local-crafts-and-traditions", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_discover-local-crafts-and-traditions.jpg" + }, + { + "name": "Hot Air Balloon Ride over the Alps", + "description": "Experience the breathtaking beauty of the French Alps from a unique perspective with a hot air balloon ride. Soar above snow-capped peaks, charming villages, and lush valleys, taking in panoramic views that will leave you speechless. This unforgettable experience is perfect for a romantic getaway or a special occasion.", + "locationName": "Various locations throughout the French Alps", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "french-alps", + "ref": "hot-air-balloon-ride-over-the-alps", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_hot-air-balloon-ride-over-the-alps.jpg" + }, + { + "name": "Via Ferrata Climbing", + "description": "Embark on a thrilling adventure with via ferrata climbing, a unique experience that combines hiking with rock climbing. Traverse secured routes along cliffs and rock faces, enjoying stunning views and an adrenaline rush. With various difficulty levels available, it's an activity suitable for both beginners and experienced climbers.", + "locationName": "Various locations throughout the French Alps, such as Les Gaillands near Chamonix", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "french-alps", + "ref": "via-ferrata-climbing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_via-ferrata-climbing.jpg" + }, + { + "name": "Visit a Traditional Alpine Village", + "description": "Step back in time and immerse yourself in the charm of a traditional Alpine village. Explore cobblestone streets lined with quaint shops, historic churches, and traditional houses adorned with flower boxes. Savor local delicacies at a cozy café and enjoy the peaceful atmosphere of village life.", + "locationName": "Villages like Megève, Samoëns, or Yvoire", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-alps", + "ref": "visit-a-traditional-alpine-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_visit-a-traditional-alpine-village.jpg" + }, + { + "name": "Lake Annecy Boat Tour", + "description": "Cruise along the crystal-clear waters of Lake Annecy, surrounded by breathtaking mountain scenery. Enjoy the fresh air and sunshine as you admire the picturesque villages, medieval castles, and lush greenery that line the shores. Opt for a guided tour to learn about the history and culture of the region, or simply relax and soak up the beauty of your surroundings.", + "locationName": "Lake Annecy", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-alps", + "ref": "lake-annecy-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_lake-annecy-boat-tour.jpg" + }, + { + "name": "Wine Tasting in the Savoie Region", + "description": "Discover the unique flavors of the Savoie wine region with a visit to a local vineyard. Sample a variety of wines, from crisp whites to robust reds, and learn about the traditional winemaking techniques of the region. Enjoy the scenic beauty of the vineyards and indulge in a delightful culinary experience.", + "locationName": "Vineyards in the Savoie region, such as Apremont or Chignin", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "french-alps", + "ref": "wine-tasting-in-the-savoie-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-alps_wine-tasting-in-the-savoie-region.jpg" + }, + { + "name": "Scuba Diving in Rangiroa", + "description": "Embark on an underwater adventure in Rangiroa, known for its vast lagoon and diverse marine life. Explore vibrant coral reefs, encounter sharks, manta rays, and a kaleidoscope of tropical fish. Whether you're a seasoned diver or a beginner, Rangiroa offers unforgettable scuba diving experiences.", + "locationName": "Rangiroa", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-polynesia", + "ref": "scuba-diving-in-rangiroa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_scuba-diving-in-rangiroa.jpg" + }, + { + "name": "Overwater Bungalow Relaxation", + "description": "Indulge in the ultimate luxury experience by staying in an overwater bungalow. Wake up to breathtaking ocean views, step directly into the turquoise waters from your private deck, and enjoy the tranquility of your secluded haven. This is the perfect way to unwind and soak in the beauty of French Polynesia.", + "locationName": "Various islands", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "french-polynesia", + "ref": "overwater-bungalow-relaxation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_overwater-bungalow-relaxation.jpg" + }, + { + "name": "Island Hopping Adventure", + "description": "Explore the diverse islands of French Polynesia on an island-hopping adventure. Discover the unique charm of each island, from the lush rainforests of Tahiti to the white-sand beaches of Bora Bora. Experience different cultures, landscapes, and activities, creating lasting memories of your Polynesian journey.", + "locationName": "Various islands", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-polynesia", + "ref": "island-hopping-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_island-hopping-adventure.jpg" + }, + { + "name": "Sunset Cruise with Polynesian Dinner", + "description": "Set sail on a romantic sunset cruise and savor a delicious Polynesian dinner. Enjoy breathtaking views of the islands as the sky transforms into a canvas of vibrant colors. Indulge in traditional dishes and immerse yourself in the Polynesian culture with live music and dance performances.", + "locationName": "Various islands", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-polynesia", + "ref": "sunset-cruise-with-polynesian-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_sunset-cruise-with-polynesian-dinner.jpg" + }, + { + "name": "Hiking and Exploring Mount Otemanu", + "description": "For adventure enthusiasts, hike to the summit of Mount Otemanu in Bora Bora. Enjoy panoramic views of the island and its stunning lagoon. The challenging hike rewards you with breathtaking scenery and a sense of accomplishment.", + "locationName": "Bora Bora", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "french-polynesia", + "ref": "hiking-and-exploring-mount-otemanu", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_hiking-and-exploring-mount-otemanu.jpg" + }, + { + "name": "Jet Skiing around Bora Bora Lagoon", + "description": "Experience the thrill of gliding across the turquoise waters of Bora Bora's iconic lagoon on a jet ski. Feel the wind in your hair as you zip past luxurious overwater bungalows, secluded beaches, and breathtaking volcanic peaks. You can even stop for a swim or snorkel in the crystal-clear waters.", + "locationName": "Bora Bora Lagoon", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "french-polynesia", + "ref": "jet-skiing-around-bora-bora-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_jet-skiing-around-bora-bora-lagoon.jpg" + }, + { + "name": "Black Pearl Farm Tour in Taha'a", + "description": "Delve into the fascinating world of Tahitian black pearls on a guided tour of a pearl farm. Learn about the unique cultivation process, admire the iridescent beauty of these precious gems, and even have the opportunity to purchase your own piece of Polynesian treasure.", + "locationName": "Taha'a Island", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-polynesia", + "ref": "black-pearl-farm-tour-in-taha-a", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_black-pearl-farm-tour-in-taha-a.jpg" + }, + { + "name": "Traditional Polynesian Dance Performance", + "description": "Immerse yourself in the vibrant culture of French Polynesia by attending a captivating Polynesian dance performance. Witness the graceful movements, rhythmic drumming, and colorful costumes that tell stories of ancient legends and island life.", + "locationName": "Various locations across the islands", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-polynesia", + "ref": "traditional-polynesian-dance-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_traditional-polynesian-dance-performance.jpg" + }, + { + "name": "4x4 Safari Adventure in Moorea", + "description": "Embark on a thrilling 4x4 safari adventure through the lush interior of Moorea. Traverse rugged mountain trails, discover hidden waterfalls, and enjoy panoramic views of the island's stunning coastline and volcanic peaks. This off-road experience is perfect for adventure seekers.", + "locationName": "Moorea Island", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-polynesia", + "ref": "4x4-safari-adventure-in-moorea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_4x4-safari-adventure-in-moorea.jpg" + }, + { + "name": "Sunset Cocktails at a Beach Bar", + "description": "Unwind and soak up the breathtaking Polynesian sunset with a refreshing cocktail at a beachfront bar. Enjoy the laid-back atmosphere, listen to the gentle waves lapping against the shore, and create unforgettable memories as the sky transforms into a canvas of vibrant colors.", + "locationName": "Various locations across the islands", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "french-polynesia", + "ref": "sunset-cocktails-at-a-beach-bar", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_sunset-cocktails-at-a-beach-bar.jpg" + }, + { + "name": "Whale Watching Excursion", + "description": "Embark on a magical journey to witness the awe-inspiring humpback whales that migrate to the warm waters of French Polynesia between July and November. Observe these gentle giants breaching, tail-slapping, and singing their haunting songs in their natural habitat. This unforgettable experience offers a unique opportunity to connect with nature and appreciate the beauty of these majestic creatures.", + "locationName": "Various islands, including Moorea and Rurutu", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-polynesia", + "ref": "whale-watching-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_whale-watching-excursion.jpg" + }, + { + "name": "Polynesian Cooking Class", + "description": "Delve into the heart of Polynesian culture by participating in a hands-on cooking class. Learn the secrets of preparing traditional dishes like poisson cru (marinated fish salad), poulet fafa (chicken with coconut milk and taro leaves), and poe (sweet pudding made from taro or banana). Discover the unique flavors and ingredients of the islands while creating delicious meals to share with your loved ones.", + "locationName": "Various resorts and cultural centers", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-polynesia", + "ref": "polynesian-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_polynesian-cooking-class.jpg" + }, + { + "name": "Stand-Up Paddleboarding in the Lagoon", + "description": "Glide across the crystal-clear waters of the lagoon on a stand-up paddleboard, enjoying the tranquility and breathtaking views of the surrounding islands. This relaxing activity is suitable for all ages and skill levels, providing a unique perspective of the marine life below and the lush landscapes above. Opt for a guided tour or explore at your own pace, discovering hidden coves and secluded beaches.", + "locationName": "Various lagoons, including Bora Bora and Moorea", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "french-polynesia", + "ref": "stand-up-paddleboarding-in-the-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_stand-up-paddleboarding-in-the-lagoon.jpg" + }, + { + "name": "Stargazing on a Secluded Beach", + "description": "Escape the city lights and experience the magic of the Polynesian night sky. Find a secluded beach, lie back on the soft sand, and gaze up at the countless stars twinkling above. With minimal light pollution, the islands offer exceptional stargazing opportunities, allowing you to marvel at constellations, shooting stars, and the Milky Way.", + "locationName": "Various secluded beaches", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "french-polynesia", + "ref": "stargazing-on-a-secluded-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_stargazing-on-a-secluded-beach.jpg" + }, + { + "name": "Hiking to Ancient Polynesian Ruins", + "description": "Embark on a journey through time by hiking to ancient Polynesian ruins scattered across the islands. Explore the remnants of marae (sacred ceremonial sites), stone temples, and traditional villages, learning about the fascinating history and culture of the Polynesian people. These hikes offer a unique blend of adventure, cultural immersion, and breathtaking scenery.", + "locationName": "Various islands, including Raiatea and Huahine", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-polynesia", + "ref": "hiking-to-ancient-polynesian-ruins", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_hiking-to-ancient-polynesian-ruins.jpg" + }, + { + "name": "Kayaking in the Turquoise Waters", + "description": "Embark on a serene kayaking adventure through the crystal-clear lagoons of Bora Bora or Moorea. Paddle at your own pace, explore hidden coves, and marvel at the vibrant marine life below. Witness the majestic Mount Otemanu as you glide across the tranquil waters, creating unforgettable memories.", + "locationName": "Bora Bora or Moorea lagoons", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-polynesia", + "ref": "kayaking-in-the-turquoise-waters", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_kayaking-in-the-turquoise-waters.jpg" + }, + { + "name": "Indulge in a Polynesian Spa Ritual", + "description": "Escape to a world of tranquility and rejuvenation with a traditional Polynesian spa ritual. Experience the healing powers of local ingredients like coconut oil, vanilla, and fragrant flowers as skilled therapists pamper you with massages, body wraps, and facials. Emerge feeling refreshed and revitalized.", + "locationName": "Various luxury resorts and spas", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-polynesia", + "ref": "indulge-in-a-polynesian-spa-ritual", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_indulge-in-a-polynesian-spa-ritual.jpg" + }, + { + "name": "Explore the Vibrant Markets of Papeete", + "description": "Immerse yourself in the bustling atmosphere of Papeete's vibrant markets. Discover a treasure trove of local handicrafts, Polynesian art, fragrant spices, and exotic fruits. Engage with friendly vendors, sample delicious street food, and find unique souvenirs to commemorate your trip.", + "locationName": "Papeete Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-polynesia", + "ref": "explore-the-vibrant-markets-of-papeete", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_explore-the-vibrant-markets-of-papeete.jpg" + }, + { + "name": "Romantic Dinner at a Lagoon-Side Restaurant", + "description": "Indulge in a magical dining experience at a lagoon-side restaurant. Savor exquisite French Polynesian cuisine, featuring fresh seafood, tropical flavors, and innovative dishes. Enjoy breathtaking sunset views, live music, and a romantic ambiance, creating a truly unforgettable evening.", + "locationName": "Various lagoon-side restaurants", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-polynesia", + "ref": "romantic-dinner-at-a-lagoon-side-restaurant", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_romantic-dinner-at-a-lagoon-side-restaurant.jpg" + }, + { + "name": "Learn the Art of Polynesian Tattooing", + "description": "Delve into the rich cultural heritage of Polynesian tattooing. Visit a local tattoo artist and learn about the symbolism, history, and techniques behind this ancient art form. Consider getting a temporary or permanent tattoo as a unique and meaningful souvenir of your Polynesian adventure.", + "locationName": "Local tattoo studios", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "french-polynesia", + "ref": "learn-the-art-of-polynesian-tattooing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-polynesia_learn-the-art-of-polynesian-tattooing.jpg" + }, + { + "name": "Stroll Along the Promenade des Anglais", + "description": "Take a leisurely walk or bike ride along the iconic Promenade des Anglais in Nice, enjoying the stunning views of the Mediterranean Sea, the vibrant atmosphere, and the beautiful architecture.", + "locationName": "Nice", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-riviera", + "ref": "stroll-along-the-promenade-des-anglais", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_stroll-along-the-promenade-des-anglais.jpg" + }, + { + "name": "Explore the Palais des Festivals et des Congrès", + "description": "Visit the Palais des Festivals et des Congrès in Cannes, where the prestigious Cannes Film Festival takes place. Walk the red carpet, admire the stunning architecture, and learn about the history of this iconic venue.", + "locationName": "Cannes", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-riviera", + "ref": "explore-the-palais-des-festivals-et-des-congr-s", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_explore-the-palais-des-festivals-et-des-congr-s.jpg" + }, + { + "name": "Discover the Charm of Saint-Tropez", + "description": "Wander through the charming streets of Saint-Tropez, known for its luxury boutiques, art galleries, and celebrity sightings. Visit the Vieux Port, soak up the atmosphere at a sidewalk café, and relax on the beautiful beaches.", + "locationName": "Saint-Tropez", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "french-riviera", + "ref": "discover-the-charm-of-saint-tropez", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_discover-the-charm-of-saint-tropez.jpg" + }, + { + "name": "Indulge in Fine Dining", + "description": "Experience the renowned culinary scene of the French Riviera. Choose from Michelin-starred restaurants, charming bistros, or waterfront seafood restaurants to savor delicious French and Mediterranean cuisine.", + "locationName": "Various", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-riviera", + "ref": "indulge-in-fine-dining", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_indulge-in-fine-dining.jpg" + }, + { + "name": "Sail the Mediterranean Sea", + "description": "Embark on a boat tour or rent a yacht to explore the stunning coastline of the French Riviera. Enjoy swimming, sunbathing, and admiring the picturesque towns and villages from the water.", + "locationName": "Various", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "french-riviera", + "ref": "sail-the-mediterranean-sea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_sail-the-mediterranean-sea.jpg" + }, + { + "name": "Hike the Coastal Trails of Cap Ferrat", + "description": "Embark on a scenic hike along the coastal trails of Cap Ferrat, a peninsula renowned for its dramatic cliffs, hidden coves, and panoramic views of the Mediterranean Sea. Breathe in the fresh sea air as you traverse the well-maintained paths, encountering secluded beaches, lush vegetation, and perhaps even glimpses of luxurious villas.", + "locationName": "Cap Ferrat", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-riviera", + "ref": "hike-the-coastal-trails-of-cap-ferrat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_hike-the-coastal-trails-of-cap-ferrat.jpg" + }, + { + "name": "Scuba Dive in the Lerins Islands", + "description": "Discover the underwater wonders of the Lerins Islands, a group of four islands off the coast of Cannes. Dive into the crystal-clear waters and explore vibrant coral reefs, diverse marine life, and even underwater shipwrecks. Whether you're a seasoned diver or a beginner, the Lerins Islands offer an unforgettable scuba diving experience.", + "locationName": "Lerins Islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-riviera", + "ref": "scuba-dive-in-the-lerins-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_scuba-dive-in-the-lerins-islands.jpg" + }, + { + "name": "Visit the Hilltop Village of Eze", + "description": "Step back in time with a visit to the enchanting hilltop village of Eze. Wander through its narrow medieval streets, admire the charming stone houses adorned with colorful flowers, and soak in the breathtaking views of the coastline from the Jardin Exotique. Explore the Fragonard perfume factory and discover the art of fragrance creation.", + "locationName": "Eze", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-riviera", + "ref": "visit-the-hilltop-village-of-eze", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_visit-the-hilltop-village-of-eze.jpg" + }, + { + "name": "Experience the Thrill of Monaco", + "description": "Take a day trip to the glamorous principality of Monaco, known for its opulent casinos, luxurious yachts, and the Formula One Grand Prix. Visit the Prince's Palace, explore the Oceanographic Museum, or try your luck at the iconic Monte Carlo Casino. In the evening, enjoy the vibrant nightlife scene with its exclusive clubs and bars.", + "locationName": "Monaco", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "french-riviera", + "ref": "experience-the-thrill-of-monaco", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_experience-the-thrill-of-monaco.jpg" + }, + { + "name": "Indulge in Local Flavors at a Provençal Market", + "description": "Immerse yourself in the vibrant atmosphere of a traditional Provençal market. Browse through stalls overflowing with fresh produce, local cheeses, fragrant spices, and handcrafted souvenirs. Sample regional specialties such as socca (chickpea pancake) and tapenade (olive spread), and discover the culinary delights of the region.", + "locationName": "Various towns and villages", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-riviera", + "ref": "indulge-in-local-flavors-at-a-proven-al-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_indulge-in-local-flavors-at-a-proven-al-market.jpg" + }, + { + "name": "Kayak Along the Mediterranean Coast", + "description": "Embark on a serene kayaking adventure along the stunning coastline of the French Riviera. Paddle through crystal-clear turquoise waters, explore hidden coves, and admire the breathtaking views of the cliffs and beaches. This activity is suitable for various skill levels and offers a unique perspective of the region's natural beauty.", + "locationName": "Various locations along the coast, such as Nice, Cannes, or Antibes", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-riviera", + "ref": "kayak-along-the-mediterranean-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_kayak-along-the-mediterranean-coast.jpg" + }, + { + "name": "Wine Tasting in Provence", + "description": "Discover the renowned wines of Provence with a delightful wine tasting experience. Visit charming vineyards nestled amidst rolling hills, learn about the winemaking process, and savor a selection of exquisite local wines, including rosé, red, and white varieties. Immerse yourself in the region's rich viticulture heritage and indulge in the flavors of Provence.", + "locationName": "Vineyards in the Provence region", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "french-riviera", + "ref": "wine-tasting-in-provence", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_wine-tasting-in-provence.jpg" + }, + { + "name": "Visit the Musée Picasso in Antibes", + "description": "Art enthusiasts will appreciate a visit to the Musée Picasso in Antibes. Housed in the Château Grimaldi, the museum showcases an extensive collection of Pablo Picasso's works, including paintings, ceramics, and drawings. Explore the artist's creative journey and gain insights into his connection with the French Riviera.", + "locationName": "Antibes", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-riviera", + "ref": "visit-the-mus-e-picasso-in-antibes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_visit-the-mus-e-picasso-in-antibes.jpg" + }, + { + "name": "Experience the Glamour of the Monaco Grand Prix", + "description": "For thrill-seekers and motorsport enthusiasts, attending the Monaco Grand Prix is an unforgettable experience. Witness the world's most prestigious Formula One race as drivers navigate the challenging street circuit of Monte Carlo. Immerse yourself in the electric atmosphere, admire the high-performance cars, and enjoy the glamorous ambiance of this iconic event.", + "locationName": "Monte Carlo, Monaco", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "french-riviera", + "ref": "experience-the-glamour-of-the-monaco-grand-prix", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_experience-the-glamour-of-the-monaco-grand-prix.jpg" + }, + { + "name": "Enjoy a Romantic Dinner Cruise", + "description": "Indulge in a romantic evening with a dinner cruise along the French Riviera. Set sail on a luxurious yacht and savor a gourmet meal while admiring the breathtaking coastal views as the sun sets over the Mediterranean Sea. Enjoy live music, dancing, and the company of your loved one for a truly unforgettable experience.", + "locationName": "Various ports along the coast", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-riviera", + "ref": "enjoy-a-romantic-dinner-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_enjoy-a-romantic-dinner-cruise.jpg" + }, + { + "name": "Paragliding over the Mediterranean", + "description": "Experience the thrill of soaring above the stunning coastline of the French Riviera with a tandem paragliding flight. Take in breathtaking panoramic views of the turquoise waters, sandy beaches, and charming towns as you glide through the air. This exhilarating adventure offers a unique perspective of the region's beauty and is perfect for adrenaline seekers.", + "locationName": "Various locations along the coast", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-riviera", + "ref": "paragliding-over-the-mediterranean", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_paragliding-over-the-mediterranean.jpg" + }, + { + "name": "Cycling the Route du Mimosa", + "description": "Embark on a scenic cycling journey along the Route du Mimosa, a picturesque route that winds through charming villages and vibrant mimosa groves. Admire the fragrant yellow blooms, enjoy the fresh air, and discover hidden gems along the way. This leisurely activity is perfect for nature lovers and those seeking a relaxing escape.", + "locationName": "Route du Mimosa", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-riviera", + "ref": "cycling-the-route-du-mimosa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_cycling-the-route-du-mimosa.jpg" + }, + { + "name": "Exploring the Lerins Islands", + "description": "Take a boat trip to the Lerins Islands, a small archipelago located just off the coast of Cannes. Discover the historic monastery on Saint-Honorat Island, relax on the pristine beaches of Sainte-Marguerite Island, or explore the island's diverse flora and fauna. This island escape offers a tranquil retreat from the bustling mainland.", + "locationName": "Lerins Islands", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "french-riviera", + "ref": "exploring-the-lerins-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_exploring-the-lerins-islands.jpg" + }, + { + "name": "Canyoning in the Gorges du Verdon", + "description": "Embark on an adventurous canyoning expedition in the Gorges du Verdon, a stunning natural wonder known as the 'Grand Canyon of Europe'. Rappel down waterfalls, swim through crystal-clear pools, and navigate through narrow gorges. This thrilling activity is perfect for adrenaline junkies and outdoor enthusiasts.", + "locationName": "Gorges du Verdon", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "french-riviera", + "ref": "canyoning-in-the-gorges-du-verdon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_canyoning-in-the-gorges-du-verdon.jpg" + }, + { + "name": "Stargazing at the Observatoire de la Côte d'Azur", + "description": "Discover the wonders of the night sky at the Observatoire de la Côte d'Azur, a renowned astronomical observatory. Participate in a guided stargazing session, learn about constellations and planets, and marvel at the beauty of the cosmos through powerful telescopes. This unique experience offers a glimpse into the vastness of the universe.", + "locationName": "Observatoire de la Côte d'Azur", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "french-riviera", + "ref": "stargazing-at-the-observatoire-de-la-c-te-d-azur", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/french-riviera_stargazing-at-the-observatoire-de-la-c-te-d-azur.jpg" + }, + { + "name": "Snorkeling with Sea Lions at Gardner Bay", + "description": "Immerse yourself in the turquoise waters of Gardner Bay, Española Island, and swim alongside playful sea lions. Witness their incredible agility underwater as they twirl and dart around you, creating an unforgettable experience.", + "locationName": "Gardner Bay, Española Island", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "galapagos-islands", + "ref": "snorkeling-with-sea-lions-at-gardner-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_snorkeling-with-sea-lions-at-gardner-bay.jpg" + }, + { + "name": "Hiking to the Sierra Negra Volcano", + "description": "Embark on a thrilling hike to the rim of the Sierra Negra Volcano, one of the largest volcanic craters in the world. Enjoy breathtaking panoramic views of the volcanic landscape and surrounding islands. Keep an eye out for unique volcanic features and endemic plant life along the way.", + "locationName": "Sierra Negra Volcano, Isabela Island", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "galapagos-islands", + "ref": "hiking-to-the-sierra-negra-volcano", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_hiking-to-the-sierra-negra-volcano.jpg" + }, + { + "name": "Wildlife Watching at Tortuga Bay", + "description": "Stroll along the pristine white sand beach of Tortuga Bay, a haven for wildlife. Observe marine iguanas basking in the sun, sea turtles nesting on the shore, and a variety of bird species soaring overhead. This tranquil beach offers a perfect escape to connect with nature.", + "locationName": "Tortuga Bay, Santa Cruz Island", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "galapagos-islands", + "ref": "wildlife-watching-at-tortuga-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_wildlife-watching-at-tortuga-bay.jpg" + }, + { + "name": "Scuba Diving at Gordon Rocks", + "description": "Dive into the underwater world at Gordon Rocks, a renowned dive site known for its diverse marine life. Encounter hammerhead sharks, Galapagos sharks, sea turtles, rays, and schools of colorful fish. This thrilling dive is perfect for experienced divers seeking an adrenaline rush.", + "locationName": "Gordon Rocks, off the coast of Santa Cruz Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "galapagos-islands", + "ref": "scuba-diving-at-gordon-rocks", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_scuba-diving-at-gordon-rocks.jpg" + }, + { + "name": "Exploring the Charles Darwin Research Station", + "description": "Delve into the scientific and conservation efforts at the Charles Darwin Research Station. Learn about the unique flora and fauna of the Galapagos Islands, ongoing research projects, and the importance of protecting this fragile ecosystem. You can even see the famous Galapagos giant tortoises up close.", + "locationName": "Charles Darwin Research Station, Santa Cruz Island", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "galapagos-islands", + "ref": "exploring-the-charles-darwin-research-station", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_exploring-the-charles-darwin-research-station.jpg" + }, + { + "name": "Kayaking along the Coast", + "description": "Embark on a serene kayaking adventure along the stunning coastline of the Galapagos Islands. Paddle through crystal-clear waters, marvel at volcanic formations, and encounter marine life such as sea turtles, penguins, and playful sea lions. Enjoy the tranquility of the ocean and discover hidden coves and beaches.", + "locationName": "Various locations throughout the archipelago", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "galapagos-islands", + "ref": "kayaking-along-the-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_kayaking-along-the-coast.jpg" + }, + { + "name": "Birdwatching on Genovesa Island", + "description": "Genovesa Island, also known as 'Bird Island,' is a paradise for bird enthusiasts. Hike along the volcanic cliffs and witness an abundance of avian species, including red-footed boobies, Nazca boobies, frigatebirds, and swallow-tailed gulls. Capture breathtaking photos and immerse yourself in the sights and sounds of this natural aviary.", + "locationName": "Genovesa Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "galapagos-islands", + "ref": "birdwatching-on-genovesa-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_birdwatching-on-genovesa-island.jpg" + }, + { + "name": "Island Hopping Adventure", + "description": "Discover the diverse landscapes and unique wildlife of the Galapagos Islands by embarking on an island-hopping adventure. Visit multiple islands, each with its own distinct ecosystem and charm. Explore volcanic craters, hike through lush forests, and relax on pristine beaches.", + "locationName": "Various islands throughout the archipelago", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "galapagos-islands", + "ref": "island-hopping-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_island-hopping-adventure.jpg" + }, + { + "name": "Stargazing on a Remote Beach", + "description": "Escape the city lights and experience the magic of stargazing on a secluded beach in the Galapagos Islands. With minimal light pollution, the night sky comes alive with a breathtaking display of stars and constellations. Relax on the sand, listen to the soothing sounds of the ocean, and marvel at the wonders of the universe.", + "locationName": "Various secluded beaches throughout the archipelago", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "galapagos-islands", + "ref": "stargazing-on-a-remote-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_stargazing-on-a-remote-beach.jpg" + }, + { + "name": "Sunset Cruise with Local Cuisine", + "description": "Indulge in a romantic sunset cruise while savoring the flavors of local Ecuadorian cuisine. Sail along the coastline, witness stunning views of the islands bathed in golden light, and enjoy a delicious meal prepared with fresh, local ingredients. Create unforgettable memories as you toast to the beauty of the Galapagos Islands.", + "locationName": "Various departure points throughout the archipelago", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "galapagos-islands", + "ref": "sunset-cruise-with-local-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_sunset-cruise-with-local-cuisine.jpg" + }, + { + "name": "Horseback Riding in the Highlands", + "description": "Explore the lush highlands of Santa Cruz Island on horseback, traversing volcanic landscapes and encountering endemic flora and fauna like giant tortoises in their natural habitat. This gentle adventure offers stunning views and a unique perspective of the island's diverse ecosystems. ", + "locationName": "Santa Cruz Island", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "galapagos-islands", + "ref": "horseback-riding-in-the-highlands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_horseback-riding-in-the-highlands.jpg" + }, + { + "name": "Panga Ride Through the Mangroves", + "description": "Embark on a relaxing panga (small boat) ride through the intricate mangrove forests of the Galapagos. Observe marine life such as sea turtles, rays, and various bird species as you navigate the calm waters and learn about the ecological importance of these unique ecosystems.", + "locationName": "Various Islands (Santa Cruz, Isabela, Fernandina)", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "galapagos-islands", + "ref": "panga-ride-through-the-mangroves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_panga-ride-through-the-mangroves.jpg" + }, + { + "name": "Visit the Wall of Tears", + "description": "Delve into the history of Isabela Island with a visit to the Wall of Tears, a historical landmark built by prisoners in the 1940s and 50s. Learn about the penal colony's past and enjoy panoramic views of the island's coastline.", + "locationName": "Isabela Island", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "galapagos-islands", + "ref": "visit-the-wall-of-tears", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_visit-the-wall-of-tears.jpg" + }, + { + "name": "Biking on Isabela Island", + "description": "Rent a bike and explore the scenic landscapes of Isabela Island at your own pace. Cycle through charming villages, along coastal paths, and to secluded beaches, enjoying the freedom to discover hidden gems and connect with the island's natural beauty.", + "locationName": "Isabela Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "galapagos-islands", + "ref": "biking-on-isabela-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_biking-on-isabela-island.jpg" + }, + { + "name": "Indulge in Local Cuisine", + "description": "Experience the flavors of the Galapagos by sampling local cuisine at restaurants and markets. Savor fresh seafood dishes, traditional Ecuadorian specialties, and tropical fruits while immersing yourself in the island's culinary culture.", + "locationName": "Various Islands (Santa Cruz, San Cristobal, Isabela)", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "galapagos-islands", + "ref": "indulge-in-local-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_indulge-in-local-cuisine.jpg" + }, + { + "name": "Surfing at Tortuga Bay", + "description": "Catch some waves at the renowned Tortuga Bay, known for its consistent swells and pristine beach. Whether you're a seasoned surfer or a beginner eager to learn, Tortuga Bay offers a thrilling experience amidst stunning natural beauty. Rent a board or join a surf lesson to ride the waves like a pro.", + "locationName": "Tortuga Bay", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "galapagos-islands", + "ref": "surfing-at-tortuga-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_surfing-at-tortuga-bay.jpg" + }, + { + "name": "Volunteer at a Conservation Project", + "description": "Contribute to the preservation of the Galapagos Islands by participating in a volunteer program. Assist with habitat restoration, wildlife monitoring, or community outreach initiatives. Immerse yourself in the local culture and make a meaningful impact on this unique ecosystem.", + "locationName": "Various locations across the islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "galapagos-islands", + "ref": "volunteer-at-a-conservation-project", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_volunteer-at-a-conservation-project.jpg" + }, + { + "name": "Photography Expedition", + "description": "Embark on a photography expedition to capture the extraordinary biodiversity and landscapes of the Galapagos. Join a guided tour led by a professional photographer who will help you find the perfect shots of iconic wildlife, volcanic formations, and breathtaking seascapes. Enhance your skills and create lasting memories through your lens.", + "locationName": "Various islands and locations", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "galapagos-islands", + "ref": "photography-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_photography-expedition.jpg" + }, + { + "name": "Relaxation and Wellness Retreat", + "description": "Escape the hustle and bustle of everyday life and indulge in a rejuvenating wellness retreat. Choose from a variety of options, including yoga sessions on the beach, spa treatments with local ingredients, and meditation amidst the tranquil natural surroundings. Reconnect with yourself and find inner peace in this island paradise.", + "locationName": "Various resorts and retreat centers", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "galapagos-islands", + "ref": "relaxation-and-wellness-retreat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_relaxation-and-wellness-retreat.jpg" + }, + { + "name": "Island-Hopping by Yacht", + "description": "Experience the ultimate luxury and exclusivity by exploring the Galapagos Islands aboard a private yacht. Customize your itinerary to visit secluded coves, pristine beaches, and hidden gems. Enjoy personalized service, gourmet meals, and unparalleled views of the archipelago's natural wonders.", + "locationName": "Various islands and routes", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "galapagos-islands", + "ref": "island-hopping-by-yacht", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galapagos-islands_island-hopping-by-yacht.jpg" + }, + { + "name": "Hike the Camino de Santiago", + "description": "Embark on a spiritual journey along the Camino de Santiago, a network of ancient pilgrimage routes leading to the Cathedral of Santiago de Compostela. Choose from various routes, such as the popular Camino Frances or the coastal Camino del Norte, and experience the breathtaking landscapes, charming villages, and camaraderie of fellow pilgrims.", + "locationName": "Various routes throughout Galicia", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "galicia", + "ref": "hike-the-camino-de-santiago", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_hike-the-camino-de-santiago.jpg" + }, + { + "name": "Explore the Historic City of Santiago de Compostela", + "description": "Wander through the cobbled streets of Santiago de Compostela, a UNESCO World Heritage Site and the culmination of the Camino de Santiago. Marvel at the magnificent Cathedral, explore the charming squares and plazas, and visit the vibrant Mercado de Abastos for a taste of Galician cuisine.", + "locationName": "Santiago de Compostela", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "galicia", + "ref": "explore-the-historic-city-of-santiago-de-compostela", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_explore-the-historic-city-of-santiago-de-compostela.jpg" + }, + { + "name": "Discover the Cíes Islands", + "description": "Escape to the Cíes Islands, an archipelago off the coast of Galicia known for its pristine beaches, crystal-clear waters, and abundant wildlife. Relax on the white sands of Rodas Beach, hike to the lighthouse for panoramic views, and enjoy a boat trip to spot dolphins and seabirds.", + "locationName": "Cíes Islands", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "galicia", + "ref": "discover-the-c-es-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_discover-the-c-es-islands.jpg" + }, + { + "name": "Indulge in Galician Cuisine", + "description": "Delight your taste buds with the fresh and flavorful cuisine of Galicia. Sample the region's famous seafood, such as octopus, scallops, and mussels, paired with local Albariño wine. Explore the charming tapas bars and traditional restaurants in towns like A Coruña and Vigo, and savor the unique culinary experience.", + "locationName": "Various locations throughout Galicia", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "galicia", + "ref": "indulge-in-galician-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_indulge-in-galician-cuisine.jpg" + }, + { + "name": "Experience the Rías Baixas Wine Region", + "description": "Embark on a wine tour through the Rías Baixas region, known for its crisp and aromatic Albariño wines. Visit charming wineries, learn about the winemaking process, and indulge in tastings of this renowned white wine. Enjoy the scenic vineyards and picturesque coastal landscapes.", + "locationName": "Rías Baixas region", + "duration": 5, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "galicia", + "ref": "experience-the-r-as-baixas-wine-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_experience-the-r-as-baixas-wine-region.jpg" + }, + { + "name": "Kayaking in the Rías Baixas", + "description": "Embark on a serene kayaking adventure through the Rías Baixas, a series of estuaries renowned for their stunning natural beauty. Paddle along calm waters, surrounded by verdant landscapes and charming coastal villages. Discover hidden coves, observe diverse marine life, and soak in the tranquility of the Galician coast.", + "locationName": "Rías Baixas", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "galicia", + "ref": "kayaking-in-the-r-as-baixas", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_kayaking-in-the-r-as-baixas.jpg" + }, + { + "name": "Surfing on the Atlantic Coast", + "description": "Galicia's rugged coastline boasts some of the best surfing conditions in Europe. Catch thrilling waves at renowned surf spots like Praia de Pantín or Praia de Razo. Whether you're a seasoned surfer or a beginner, you'll find the perfect wave to ride and experience the adrenaline of this exhilarating water sport.", + "locationName": "Praia de Pantín or Praia de Razo", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "galicia", + "ref": "surfing-on-the-atlantic-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_surfing-on-the-atlantic-coast.jpg" + }, + { + "name": "Whale Watching Excursion", + "description": "Embark on an unforgettable whale watching excursion off the Galician coast. Witness the majestic presence of these gentle giants as they migrate through the Atlantic waters. Observe various whale species, such as humpback whales, fin whales, and dolphins, in their natural habitat. This awe-inspiring experience is perfect for nature enthusiasts and wildlife lovers.", + "locationName": "Galician Coast", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "galicia", + "ref": "whale-watching-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_whale-watching-excursion.jpg" + }, + { + "name": "Explore the Tower of Hercules", + "description": "Step back in time and visit the Tower of Hercules, an ancient Roman lighthouse and the oldest functioning lighthouse in the world. Climb to the top for breathtaking panoramic views of the city of A Coruña and the surrounding coastline. Explore the fascinating history and legends surrounding this iconic landmark.", + "locationName": "A Coruña", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "galicia", + "ref": "explore-the-tower-of-hercules", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_explore-the-tower-of-hercules.jpg" + }, + { + "name": "Discover the Lugo Roman Walls", + "description": "Walk along the ancient Lugo Roman Walls, a UNESCO World Heritage Site and one of the best-preserved Roman fortifications in the world. Explore the historic city center of Lugo, visit the Roman baths, and immerse yourself in the rich history of this charming Galician city.", + "locationName": "Lugo", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "galicia", + "ref": "discover-the-lugo-roman-walls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_discover-the-lugo-roman-walls.jpg" + }, + { + "name": "Hot Springs Relaxation", + "description": "Unwind and rejuvenate in Galicia's natural hot springs. Numerous thermal bath complexes are scattered throughout the region, offering a variety of therapeutic and relaxing experiences. Immerse yourself in the mineral-rich waters, surrounded by stunning landscapes, and let the stress melt away.", + "locationName": "Ourense", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "galicia", + "ref": "hot-springs-relaxation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_hot-springs-relaxation.jpg" + }, + { + "name": "Explore the Ribeira Sacra", + "description": "Embark on a scenic journey through the Ribeira Sacra, a stunning region known for its dramatic canyons, terraced vineyards, and historic monasteries. Take a boat trip along the Sil River, hike through the vineyards, and visit ancient religious sites, enjoying breathtaking views and a taste of Galician wine culture.", + "locationName": "Ribeira Sacra", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "galicia", + "ref": "explore-the-ribeira-sacra", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_explore-the-ribeira-sacra.jpg" + }, + { + "name": "Visit the Castro de Santa Trega", + "description": "Step back in time at the Castro de Santa Trega, an ancient Celtic settlement perched on a hilltop overlooking the Atlantic Ocean. Explore the remains of stone houses, fortifications, and learn about the fascinating history and culture of the Celts who once inhabited this region.", + "locationName": "A Guarda", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "galicia", + "ref": "visit-the-castro-de-santa-trega", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_visit-the-castro-de-santa-trega.jpg" + }, + { + "name": "Discover the Islas Atlánticas National Park", + "description": "Embark on a boat tour to the Islas Atlánticas National Park, a stunning archipelago off the coast of Galicia. Explore the unique ecosystems of the islands, home to diverse birdlife, marine species, and breathtaking landscapes. Hike to the lighthouses, relax on secluded beaches, and enjoy the tranquility of this protected natural paradise.", + "locationName": "Islas Atlánticas", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "galicia", + "ref": "discover-the-islas-atl-nticas-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_discover-the-islas-atl-nticas-national-park.jpg" + }, + { + "name": "Experience Galician Festivals", + "description": "Immerse yourself in the vibrant culture of Galicia by attending one of the many traditional festivals held throughout the year. From lively music and dance performances to religious processions and gastronomic celebrations, these events offer a unique opportunity to experience the local customs and traditions.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "galicia", + "ref": "experience-galician-festivals", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_experience-galician-festivals.jpg" + }, + { + "name": "Go horseback riding through the Galician countryside", + "description": "Embark on a scenic horseback riding adventure through Galicia's lush landscapes, taking in rolling hills, verdant forests, and charming villages. This activity is perfect for nature enthusiasts and animal lovers, offering a unique perspective of the region's beauty and tranquility.", + "locationName": "Various locations throughout Galicia", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "galicia", + "ref": "go-horseback-riding-through-the-galician-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_go-horseback-riding-through-the-galician-countryside.jpg" + }, + { + "name": "Take a boat trip to the Islas Cíes", + "description": "Embark on a boat trip to the stunning Islas Cíes, an archipelago off the coast of Galicia known for its pristine beaches, crystal-clear waters, and diverse marine life. Spend the day swimming, sunbathing, and exploring the islands' natural beauty. Keep an eye out for dolphins and other marine creatures during the journey.", + "locationName": "Islas Cíes", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "galicia", + "ref": "take-a-boat-trip-to-the-islas-c-es", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_take-a-boat-trip-to-the-islas-c-es.jpg" + }, + { + "name": "Visit the Cathedral of Santiago de Compostela", + "description": "Explore the magnificent Cathedral of Santiago de Compostela, the culmination of the Camino de Santiago pilgrimage and a masterpiece of Romanesque architecture. Marvel at its intricate facade, ornate interior, and the tomb of St. James. Join a guided tour to delve into the cathedral's history and significance.", + "locationName": "Santiago de Compostela", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "galicia", + "ref": "visit-the-cathedral-of-santiago-de-compostela", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_visit-the-cathedral-of-santiago-de-compostela.jpg" + }, + { + "name": "Explore the fishing villages along the coast", + "description": "Discover the charming fishing villages that dot Galicia's coastline, each with its own unique character and traditions. Wander through narrow streets, admire colorful houses, and savor the freshest seafood at local restaurants. Immerse yourself in the authentic atmosphere and maritime heritage of these coastal gems.", + "locationName": "Various coastal villages in Galicia", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "galicia", + "ref": "explore-the-fishing-villages-along-the-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_explore-the-fishing-villages-along-the-coast.jpg" + }, + { + "name": "Attend a traditional Galician festival", + "description": "Immerse yourself in the vibrant culture of Galicia by attending a traditional festival. Experience lively music, folk dances, and colorful costumes, and sample local delicacies. These festivals offer a glimpse into the region's rich heritage and provide a truly unforgettable cultural experience.", + "locationName": "Various locations throughout Galicia", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "galicia", + "ref": "attend-a-traditional-galician-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/galicia_attend-a-traditional-galician-festival.jpg" + }, + { + "name": "Grand Canyon South Rim Trail", + "description": "Embark on a scenic hike along the South Rim Trail, offering breathtaking panoramic views of the canyon's vastness and geological layers. Choose from various trail segments, like the paved Rim Trail or the challenging Bright Angel Trail, catering to different fitness levels. Witness iconic landmarks such as Mather Point and Yavapai Point, capturing stunning photographs and creating unforgettable memories.", + "locationName": "Grand Canyon South Rim", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "grand-canyon", + "ref": "grand-canyon-south-rim-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_grand-canyon-south-rim-trail.jpg" + }, + { + "name": "Mule Rides into the Canyon", + "description": "Experience the thrill of descending into the canyon on a mule, traversing scenic trails and enjoying unique perspectives of the majestic landscape. Opt for a half-day or full-day ride, guided by experienced wranglers who share insights about the canyon's history and geology. This iconic Grand Canyon activity offers a memorable adventure suitable for families and nature enthusiasts.", + "locationName": "Grand Canyon Village", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "grand-canyon", + "ref": "mule-rides-into-the-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_mule-rides-into-the-canyon.jpg" + }, + { + "name": "Whitewater Rafting on the Colorado River", + "description": "Embark on an exhilarating whitewater rafting expedition down the Colorado River, navigating through the heart of the Grand Canyon. Choose from multi-day adventures camping under the stars or shorter day trips. Experience the thrill of rapids, marvel at towering canyon walls, and witness the raw power of nature. This adrenaline-pumping activity is perfect for adventure seekers and nature lovers.", + "locationName": "Colorado River", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "grand-canyon", + "ref": "whitewater-rafting-on-the-colorado-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_whitewater-rafting-on-the-colorado-river.jpg" + }, + { + "name": "Grand Canyon Skywalk", + "description": "Step onto the glass-bottom Skywalk, extending 70 feet over the canyon's edge, and experience a thrilling sensation of walking on air. Enjoy unparalleled panoramic views of the canyon floor and the Colorado River below. Capture breathtaking photos and create lasting memories of this unique and exhilarating experience.", + "locationName": "Grand Canyon West Rim", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "grand-canyon", + "ref": "grand-canyon-skywalk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_grand-canyon-skywalk.jpg" + }, + { + "name": "Grand Canyon Stargazing", + "description": "Escape the city lights and immerse yourself in the wonders of the night sky at the Grand Canyon. Join a ranger-led stargazing program or find a secluded spot to witness the brilliance of the Milky Way and constellations. The Grand Canyon's dark skies offer an exceptional opportunity to connect with the universe and appreciate the beauty of the cosmos.", + "locationName": "Various locations within the park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "grand-canyon", + "ref": "grand-canyon-stargazing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_grand-canyon-stargazing.jpg" + }, + { + "name": "Scenic Helicopter Tour over the Grand Canyon", + "description": "Embark on a breathtaking helicopter tour and witness the grandeur of the Grand Canyon from a unique aerial perspective. Soar above the rim, marvel at the intricate layers of rock formations, and capture stunning panoramic views of the vast canyon landscape. This exhilarating experience offers an unforgettable way to appreciate the immense scale and beauty of this natural wonder.", + "locationName": "Grand Canyon National Park", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "grand-canyon", + "ref": "scenic-helicopter-tour-over-the-grand-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_scenic-helicopter-tour-over-the-grand-canyon.jpg" + }, + { + "name": "Explore the Historic Grand Canyon Village", + "description": "Step back in time and discover the rich history of the Grand Canyon at the Grand Canyon Village. Visit the historic El Tovar Hotel, a landmark lodge with a fascinating past, and explore the Hopi House, a unique building showcasing Native American arts and crafts. Immerse yourself in the cultural heritage of the region through exhibits at the Yavapai Museum and Verkamp's Visitor Center. This journey through time offers insights into the people and events that shaped the Grand Canyon.", + "locationName": "Grand Canyon Village", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "grand-canyon", + "ref": "explore-the-historic-grand-canyon-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_explore-the-historic-grand-canyon-village.jpg" + }, + { + "name": "Bike the South Rim Trail", + "description": "Enjoy a leisurely bike ride along the scenic South Rim Trail, offering breathtaking views of the Grand Canyon at every turn. Rent a bicycle and explore the paved path, stopping at various viewpoints to admire the vastness and beauty of the canyon. This family-friendly activity provides a relaxing way to experience the natural splendor of the Grand Canyon at your own pace.", + "locationName": "South Rim Trail", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "grand-canyon", + "ref": "bike-the-south-rim-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_bike-the-south-rim-trail.jpg" + }, + { + "name": "Jeep Tour through the Desert Landscape", + "description": "Embark on an adventurous jeep tour through the rugged desert landscape surrounding the Grand Canyon. Explore hidden trails, discover ancient Native American ruins, and witness the unique flora and fauna of the region. This off-road experience offers a thrilling way to explore the diverse ecosystems and geological wonders beyond the canyon rim.", + "locationName": "Grand Canyon National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "grand-canyon", + "ref": "jeep-tour-through-the-desert-landscape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_jeep-tour-through-the-desert-landscape.jpg" + }, + { + "name": "Visit the Desert View Watchtower", + "description": "Journey to the eastern edge of the Grand Canyon and discover the Desert View Watchtower, a historic landmark offering panoramic views of the canyon and the Colorado River. Climb to the top of the tower for breathtaking vistas, explore the unique architecture inspired by Native American designs, and learn about the cultural significance of this iconic structure.", + "locationName": "Desert View Watchtower", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "grand-canyon", + "ref": "visit-the-desert-view-watchtower", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_visit-the-desert-view-watchtower.jpg" + }, + { + "name": "Havasupai Hike and Camping", + "description": "Embark on a challenging yet rewarding hike to the turquoise waterfalls and swimming holes of Havasupai, an indigenous village located within the Grand Canyon National Park. Camp under the stars and immerse yourself in the natural beauty of this secluded oasis. Reservations are required well in advance due to its popularity.", + "locationName": "Havasupai Indian Reservation", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "grand-canyon", + "ref": "havasupai-hike-and-camping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_havasupai-hike-and-camping.jpg" + }, + { + "name": "Grand Canyon Railway Adventure", + "description": "Take a nostalgic journey on the Grand Canyon Railway, a historic train that travels from Williams, Arizona to the South Rim of the Grand Canyon. Enjoy the scenic views and onboard entertainment, including Wild West shows and live music, as you travel back in time.", + "locationName": "Grand Canyon Railway Depot", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "grand-canyon", + "ref": "grand-canyon-railway-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_grand-canyon-railway-adventure.jpg" + }, + { + "name": "Geological Museum Exploration", + "description": "Discover the fascinating geological history of the Grand Canyon at the Yavapai Geology Museum. Learn about the formation of the canyon, the different rock layers, and the fossils that have been found in the area. Interactive exhibits and knowledgeable rangers make this a great educational experience for all ages.", + "locationName": "Yavapai Geology Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "grand-canyon", + "ref": "geological-museum-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_geological-museum-exploration.jpg" + }, + { + "name": "Sunset Viewing at Hopi Point", + "description": "Witness a breathtaking sunset over the Grand Canyon from Hopi Point, a popular viewpoint on the South Rim. The vibrant colors of the sky reflecting off the canyon walls create a truly magical experience. Arrive early to secure a good spot, as this location tends to get crowded.", + "locationName": "Hopi Point", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "grand-canyon", + "ref": "sunset-viewing-at-hopi-point", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_sunset-viewing-at-hopi-point.jpg" + }, + { + "name": "Ranger-led Programs and Nature Walks", + "description": "Join a park ranger for a guided nature walk or attend one of the many ranger-led programs offered at the Grand Canyon. Learn about the park's history, geology, flora, and fauna while gaining a deeper appreciation for this natural wonder. These programs are a great way to enhance your visit and learn from experts.", + "locationName": "Various locations within the park", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "grand-canyon", + "ref": "ranger-led-programs-and-nature-walks", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_ranger-led-programs-and-nature-walks.jpg" + }, + { + "name": "Rock Climbing", + "description": "Challenge yourself with a thrilling rock climbing experience on the cliffs surrounding the Grand Canyon. With various routes for different skill levels, both beginners and experienced climbers can enjoy the breathtaking views and adrenaline rush.", + "locationName": "Grand Canyon National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "grand-canyon", + "ref": "rock-climbing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_rock-climbing.jpg" + }, + { + "name": "Grand Canyon North Rim Visit", + "description": "Escape the crowds and discover the serene beauty of the Grand Canyon's North Rim. Hike through lush forests, enjoy panoramic viewpoints, and visit the historic Grand Canyon Lodge for a peaceful retreat.", + "locationName": "Grand Canyon North Rim", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "grand-canyon", + "ref": "grand-canyon-north-rim-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_grand-canyon-north-rim-visit.jpg" + }, + { + "name": "Photography Tour", + "description": "Capture the awe-inspiring landscapes of the Grand Canyon on a guided photography tour. Learn professional tips and tricks while discovering hidden gems and iconic viewpoints, creating unforgettable memories and stunning photos.", + "locationName": "Various locations within the park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "grand-canyon", + "ref": "photography-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_photography-tour.jpg" + }, + { + "name": "Visit the Tusayan Museum and Ruin", + "description": "Step back in time and explore the ancient history of the Ancestral Puebloan people at the Tusayan Museum and Ruin. Discover archaeological artifacts, learn about their culture and traditions, and walk among the remnants of their dwellings.", + "locationName": "Tusayan Museum and Ruin", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "grand-canyon", + "ref": "visit-the-tusayan-museum-and-ruin", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_visit-the-tusayan-museum-and-ruin.jpg" + }, + { + "name": "Scenic Drive along Desert View Drive", + "description": "Embark on a scenic drive along Desert View Drive, stopping at various viewpoints and historical landmarks. Enjoy breathtaking panoramas, explore the Watchtower, and witness the changing colors of the canyon walls as the sun sets.", + "locationName": "Desert View Drive", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "grand-canyon", + "ref": "scenic-drive-along-desert-view-drive", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/grand-canyon_scenic-drive-along-desert-view-drive.jpg" + }, + { + "name": "Snorkeling Adventure on the Outer Reef", + "description": "Embark on a full-day snorkeling tour to the outer reef, where you'll discover vibrant coral gardens teeming with colorful fish, sea turtles, and other marine life. Explore different snorkel sites, enjoy a delicious lunch on board, and relax on the sun-drenched deck while cruising through turquoise waters.", + "locationName": "Outer Great Barrier Reef", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-barrier-reef", + "ref": "snorkeling-adventure-on-the-outer-reef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_snorkeling-adventure-on-the-outer-reef.jpg" + }, + { + "name": "Scuba Diving for Certified Divers", + "description": "Dive into the underwater world of the Great Barrier Reef and witness its breathtaking beauty up close. Explore renowned dive sites, encounter diverse marine species, and experience the thrill of diving in one of the most iconic destinations on Earth. Choose from introductory dives for beginners or advanced dives for experienced divers.", + "locationName": "Various dive sites throughout the reef", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "great-barrier-reef", + "ref": "scuba-diving-for-certified-divers", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_scuba-diving-for-certified-divers.jpg" + }, + { + "name": "Scenic Helicopter Flight over the Reef", + "description": "Soar above the Great Barrier Reef on a breathtaking helicopter tour and witness its vastness and beauty from a unique perspective. Marvel at the vibrant colors of the coral reefs, spot marine life swimming below, and capture stunning aerial photographs of this natural wonder.", + "locationName": "Various departure points along the coast", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "great-barrier-reef", + "ref": "scenic-helicopter-flight-over-the-reef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_scenic-helicopter-flight-over-the-reef.jpg" + }, + { + "name": "Glass-Bottom Boat and Underwater Observatory Tour", + "description": "Experience the magic of the reef without getting wet on a glass-bottom boat tour. Observe the coral reefs and marine life through the glass bottom, or visit an underwater observatory for a closer look at the underwater ecosystem. This option is perfect for families with young children or those who prefer to stay dry.", + "locationName": "Green Island or Reefworld pontoon", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "great-barrier-reef", + "ref": "glass-bottom-boat-and-underwater-observatory-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_glass-bottom-boat-and-underwater-observatory-tour.jpg" + }, + { + "name": "Island Hopping and Beach Relaxation", + "description": "Explore the idyllic islands of the Great Barrier Reef, such as Whitsunday Island or Fitzroy Island. Relax on pristine beaches, swim in crystal-clear waters, go for a hike with stunning views, or simply soak up the tropical island vibes. Each island offers unique experiences, from luxury resorts to secluded coves.", + "locationName": "Whitsunday Islands or Fitzroy Island", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-barrier-reef", + "ref": "island-hopping-and-beach-relaxation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_island-hopping-and-beach-relaxation.jpg" + }, + { + "name": "Whitsunday Islands Sailing Adventure", + "description": "Embark on a sailing expedition through the stunning Whitsunday Islands, a collection of 74 idyllic islands within the Great Barrier Reef. Sail turquoise waters, discover secluded beaches, and snorkel among vibrant coral reefs. Opt for a day trip or a multi-day sailing adventure with overnight stays on the boat or at island resorts.", + "locationName": "Whitsunday Islands", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "great-barrier-reef", + "ref": "whitsunday-islands-sailing-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_whitsunday-islands-sailing-adventure.jpg" + }, + { + "name": "Indigenous Culture and Reef Exploration", + "description": "Immerse yourself in the rich cultural heritage of the Aboriginal people and their connection to the Great Barrier Reef. Join a guided tour led by Indigenous rangers who will share their knowledge of the reef's ecosystem, traditional fishing practices, and the spiritual significance of the land and sea.", + "locationName": "Various locations along the coast", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-barrier-reef", + "ref": "indigenous-culture-and-reef-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_indigenous-culture-and-reef-exploration.jpg" + }, + { + "name": "Reef Sleep Experience at Reefworld", + "description": "Spend an unforgettable night on the Great Barrier Reef with the Reefsleep experience at Reefworld. Enjoy a day of snorkeling, diving, and exploring the reef, followed by a delicious dinner under the stars. Sleep in a comfortable swag on the upper deck of the pontoon, surrounded by the mesmerizing sounds of the ocean.", + "locationName": "Reefworld Pontoon", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "great-barrier-reef", + "ref": "reef-sleep-experience-at-reefworld", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_reef-sleep-experience-at-reefworld.jpg" + }, + { + "name": "Kuranda Scenic Railway and Rainforest Exploration", + "description": "Embark on a scenic journey through the lush rainforests of Queensland aboard the Kuranda Scenic Railway. Travel through the rainforest canopy, past cascading waterfalls, and into the charming village of Kuranda. Explore the local markets, visit wildlife parks, or take a thrilling Skyrail Rainforest Cableway ride for breathtaking views.", + "locationName": "Kuranda", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-barrier-reef", + "ref": "kuranda-scenic-railway-and-rainforest-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_kuranda-scenic-railway-and-rainforest-exploration.jpg" + }, + { + "name": "Wildlife Encounters at Hartley's Crocodile Adventures", + "description": "Get up close to Australia's incredible wildlife at Hartley's Crocodile Adventures. Witness thrilling crocodile feeding shows, learn about cassowaries and koalas, and explore the diverse ecosystems of the park. Take a boat cruise through the melaleuca wetlands and spot native birds, reptiles, and mammals.", + "locationName": "Hartley's Crocodile Adventures", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "great-barrier-reef", + "ref": "wildlife-encounters-at-hartley-s-crocodile-adventures", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_wildlife-encounters-at-hartley-s-crocodile-adventures.jpg" + }, + { + "name": "Sunset Sailing Cruise", + "description": "Embark on a romantic and relaxing sunset sailing cruise along the Great Barrier Reef. Witness the breathtaking colors of the sky as the sun dips below the horizon, casting a golden glow over the turquoise waters. Enjoy a glass of sparkling wine and delicious canapés while taking in the stunning scenery and the tranquil atmosphere.", + "locationName": "Various locations along the Great Barrier Reef", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-barrier-reef", + "ref": "sunset-sailing-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_sunset-sailing-cruise.jpg" + }, + { + "name": "Whitehaven Beach Picnic and Swimming", + "description": "Escape to the pristine shores of Whitehaven Beach, renowned for its powdery white sand and crystal-clear waters. Enjoy a leisurely picnic on the beach, soaking up the sun and the breathtaking views. Take a refreshing swim in the turquoise waters or simply relax and unwind in this idyllic paradise.", + "locationName": "Whitsunday Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "great-barrier-reef", + "ref": "whitehaven-beach-picnic-and-swimming", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_whitehaven-beach-picnic-and-swimming.jpg" + }, + { + "name": "Kayaking and Stand-Up Paddleboarding", + "description": "Explore the calm waters and hidden coves of the Great Barrier Reef at your own pace with a kayaking or stand-up paddleboarding adventure. Glide through the mangrove forests, discover secluded beaches, and encounter diverse marine life. This activity is perfect for those seeking a more active and immersive experience.", + "locationName": "Various locations along the Great Barrier Reef", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "great-barrier-reef", + "ref": "kayaking-and-stand-up-paddleboarding", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_kayaking-and-stand-up-paddleboarding.jpg" + }, + { + "name": "Fishing Charters", + "description": "Cast your line and experience the thrill of fishing in the Great Barrier Reef. Join a fishing charter and try your luck at catching a variety of fish species, including coral trout, red emperor, and Spanish mackerel. Whether you're a seasoned angler or a beginner, this activity offers an exciting adventure on the open water.", + "locationName": "Various locations along the Great Barrier Reef", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-barrier-reef", + "ref": "fishing-charters", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_fishing-charters.jpg" + }, + { + "name": "Island Resort Stay", + "description": "Indulge in a luxurious and relaxing stay at one of the many island resorts located on the Great Barrier Reef. Enjoy world-class amenities, stunning ocean views, and access to a variety of water activities. Pamper yourself with spa treatments, savor gourmet cuisine, and create unforgettable memories in this tropical paradise.", + "locationName": "Various islands within the Great Barrier Reef", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "great-barrier-reef", + "ref": "island-resort-stay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_island-resort-stay.jpg" + }, + { + "name": "Whale Watching Expedition", + "description": "Embark on a thrilling journey to witness the majestic humpback whales during their annual migration (June-November). Observe these gentle giants breaching, tail-slapping, and spyhopping in the pristine waters of the Great Barrier Reef. Experienced guides provide insights into whale behavior and conservation efforts, making it an unforgettable and educational experience.", + "locationName": "Heron Island or Whitsunday Islands", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-barrier-reef", + "ref": "whale-watching-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_whale-watching-expedition.jpg" + }, + { + "name": "Indigenous Cultural Immersion", + "description": "Connect with the rich heritage of the Aboriginal and Torres Strait Islander peoples through an immersive cultural experience. Learn about their deep connection to the reef, participate in traditional storytelling, art workshops, or dance performances. Gain a profound appreciation for their sustainable practices and ancient wisdom.", + "locationName": "Mossman Gorge Centre or various islands", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "great-barrier-reef", + "ref": "indigenous-cultural-immersion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_indigenous-cultural-immersion.jpg" + }, + { + "name": "Low Isles Tropical Island Escape", + "description": "Escape to the idyllic Low Isles, a picturesque coral cay surrounded by turquoise waters and teeming with marine life. Relax on pristine beaches, snorkel among colorful coral gardens, or take a leisurely glass-bottom boat tour. Enjoy a picnic lunch amidst the tropical paradise and soak up the serenity of this secluded island gem.", + "locationName": "Low Isles", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-barrier-reef", + "ref": "low-isles-tropical-island-escape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_low-isles-tropical-island-escape.jpg" + }, + { + "name": "Stargazing Cruise under the Southern Sky", + "description": "Experience the magic of the night sky on a stargazing cruise. Sail away from the city lights and marvel at the brilliance of the Milky Way and constellations. Knowledgeable guides share insights into astronomy and Aboriginal starlore, making it a romantic and awe-inspiring evening.", + "locationName": "Departs from Port Douglas or Cairns", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "great-barrier-reef", + "ref": "stargazing-cruise-under-the-southern-sky", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_stargazing-cruise-under-the-southern-sky.jpg" + }, + { + "name": "Seafood Feast and Culinary Delights", + "description": "Indulge in a delectable seafood feast featuring the freshest catches of the day. Savor the flavors of locally sourced prawns, oysters, fish, and other delicacies prepared with culinary expertise. Pair your meal with regional wines and enjoy the vibrant atmosphere of waterfront restaurants or charming cafes.", + "locationName": "Port Douglas, Cairns, or various islands", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-barrier-reef", + "ref": "seafood-feast-and-culinary-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-barrier-reef_seafood-feast-and-culinary-delights.jpg" + }, + { + "name": "Grizzly Bear Viewing", + "description": "Embark on a thrilling expedition to witness the majestic grizzly bears in their natural habitat. Join a guided tour to prime viewing locations, such as the Khutzeymateen Grizzly Bear Sanctuary, where you can observe these magnificent creatures as they fish for salmon or interact with their cubs. This unforgettable experience offers a unique glimpse into the lives of these iconic animals.", + "locationName": "Khutzeymateen Grizzly Bear Sanctuary or other designated viewing areas", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "great-bear-rainforest", + "ref": "grizzly-bear-viewing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_grizzly-bear-viewing.jpg" + }, + { + "name": "Kayaking through the Fjords", + "description": "Paddle through the pristine waters of the Great Bear Rainforest's fjords, surrounded by towering mountains and lush forests. Explore hidden coves, encounter marine life such as seals and sea lions, and marvel at the breathtaking scenery. Kayaking tours cater to all skill levels, from beginners to experienced paddlers, offering a peaceful and immersive way to experience the rainforest's beauty.", + "locationName": "Various fjords and inlets throughout the rainforest", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-bear-rainforest", + "ref": "kayaking-through-the-fjords", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_kayaking-through-the-fjords.jpg" + }, + { + "name": "Whale Watching Excursions", + "description": "Set sail on a whale watching adventure to witness the awe-inspiring humpback whales, orcas, and other marine mammals that inhabit the waters of the Great Bear Rainforest. Join experienced guides who will share their knowledge about these incredible creatures and their habitat. Witness the whales breaching, tail-slapping, and socializing in their natural environment, creating memories that will last a lifetime.", + "locationName": "Departure points from various coastal towns and villages", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "great-bear-rainforest", + "ref": "whale-watching-excursions", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_whale-watching-excursions.jpg" + }, + { + "name": "Hiking the Rainforest Trails", + "description": "Lace up your hiking boots and explore the diverse trails that wind through the Great Bear Rainforest. From short and easy walks to challenging multi-day treks, there's a trail for every level of hiker. Discover ancient forests, cascading waterfalls, and panoramic viewpoints, immersing yourself in the tranquility and beauty of the rainforest.", + "locationName": "Various trailheads throughout the rainforest, including the Spirit Bear Trail and the West Coast Trail", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "great-bear-rainforest", + "ref": "hiking-the-rainforest-trails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_hiking-the-rainforest-trails.jpg" + }, + { + "name": "Indigenous Cultural Experiences", + "description": "Immerse yourself in the rich culture and traditions of the First Nations people who have called the Great Bear Rainforest home for millennia. Visit cultural centers, participate in traditional storytelling sessions, or learn about ancient art forms and spiritual practices. These experiences offer a deeper understanding of the rainforest's significance to the indigenous communities and their connection to the land.", + "locationName": "Various cultural centers and indigenous communities within the rainforest", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-bear-rainforest", + "ref": "indigenous-cultural-experiences", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_indigenous-cultural-experiences.jpg" + }, + { + "name": "Birdwatching in the Estuary", + "description": "Embark on a serene birdwatching experience in the heart of the Great Bear Rainforest's estuaries. Witness a mesmerizing array of avian life, from majestic bald eagles soaring above to vibrant migratory birds like the Pacific loon and the western sandpiper. Local guides can provide insights into the unique behaviors and habitats of these feathered wonders, making it an unforgettable experience for nature enthusiasts.", + "locationName": "Kitimat River Estuary or Bella Coola Estuary", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "great-bear-rainforest", + "ref": "birdwatching-in-the-estuary", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_birdwatching-in-the-estuary.jpg" + }, + { + "name": "Wildlife Photography Expedition", + "description": "Capture the essence of the Great Bear Rainforest's wild inhabitants on a dedicated photography expedition. Accompanied by experienced guides and professional photographers, venture into prime wildlife viewing areas, seeking encounters with black bears, wolves, orcas, and other iconic species. Learn valuable techniques for wildlife photography amidst the stunning backdrop of the rainforest.", + "locationName": "Khutzeymateen Grizzly Bear Sanctuary or various islands and inlets", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-bear-rainforest", + "ref": "wildlife-photography-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_wildlife-photography-expedition.jpg" + }, + { + "name": "Forest Bathing and Nature Meditation", + "description": "Immerse yourself in the tranquility of the rainforest with a rejuvenating forest bathing and nature meditation experience. Led by certified guides, engage in mindfulness practices and gentle walks amidst the towering trees and calming sounds of nature. Reconnect with your senses, reduce stress, and find inner peace in the heart of this pristine wilderness.", + "locationName": "Various rainforest trails and secluded areas", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "great-bear-rainforest", + "ref": "forest-bathing-and-nature-meditation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_forest-bathing-and-nature-meditation.jpg" + }, + { + "name": "Cultural Tour of First Nations Villages", + "description": "Embark on a respectful and enriching cultural tour to experience the heritage and traditions of the First Nations people who have inhabited the Great Bear Rainforest for millennia. Visit local villages, engage with community members, and gain insights into their art, storytelling, and deep connection to the land. Learn about their sustainable practices and the vital role they play in protecting the rainforest ecosystem.", + "locationName": "First Nations villages such as Hartley Bay or Klemtu", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-bear-rainforest", + "ref": "cultural-tour-of-first-nations-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_cultural-tour-of-first-nations-villages.jpg" + }, + { + "name": "Stargazing Cruise on a Silent Electric Boat", + "description": "Embark on a magical stargazing cruise aboard a silent electric boat, gliding through the calm waters of the rainforest under a canopy of stars. With minimal light pollution, marvel at the brilliance of the night sky, constellations, and the Milky Way. Knowledgeable guides can share insights into astronomy and the importance of preserving the dark sky environment.", + "locationName": "Various inlets and channels within the rainforest", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "great-bear-rainforest", + "ref": "stargazing-cruise-on-a-silent-electric-boat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_stargazing-cruise-on-a-silent-electric-boat.jpg" + }, + { + "name": "Heli-Hiking Adventure", + "description": "Embark on an exhilarating helicopter journey to remote alpine meadows and pristine mountain peaks. Hike through breathtaking landscapes, surrounded by towering glaciers and panoramic vistas, experiencing the raw beauty of the rainforest from a unique perspective.", + "locationName": "Various mountain ranges within the Great Bear Rainforest", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "great-bear-rainforest", + "ref": "heli-hiking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_heli-hiking-adventure.jpg" + }, + { + "name": "Wildlife Photography Workshop", + "description": "Join a professional wildlife photographer on a guided expedition to capture the incredible biodiversity of the Great Bear Rainforest. Learn valuable techniques for photographing elusive animals like bears, wolves, and eagles in their natural habitat, creating lasting memories of your wildlife encounters.", + "locationName": "Various locations throughout the rainforest, including estuaries, rivers, and forests", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-bear-rainforest", + "ref": "wildlife-photography-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_wildlife-photography-workshop.jpg" + }, + { + "name": "Fishing Excursion in Pristine Waters", + "description": "Cast your line into the abundant waters of the Great Bear Rainforest, teeming with salmon, halibut, and other prized fish species. Experience the thrill of sport fishing amidst stunning natural scenery, guided by experienced local fishermen who share their knowledge of the region's aquatic life.", + "locationName": "Rivers and coastal areas within the rainforest", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-bear-rainforest", + "ref": "fishing-excursion-in-pristine-waters", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_fishing-excursion-in-pristine-waters.jpg" + }, + { + "name": "Scenic Seaplane Flight", + "description": "Soar above the vast expanse of the Great Bear Rainforest in a seaplane, enjoying breathtaking aerial views of its rugged coastlines, ancient forests, and sparkling waterways. Witness the immensity and beauty of this unique ecosystem from a different perspective, capturing unforgettable memories from above.", + "locationName": "Departure from various coastal towns or villages", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "great-bear-rainforest", + "ref": "scenic-seaplane-flight", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_scenic-seaplane-flight.jpg" + }, + { + "name": "Cultural Immersion with First Nations Communities", + "description": "Engage in a meaningful cultural exchange with the indigenous communities who have called the Great Bear Rainforest home for millennia. Learn about their traditions, art, storytelling, and deep connection to the land, gaining a profound appreciation for their way of life and the rich cultural heritage of the region.", + "locationName": "First Nations villages and cultural centers within the rainforest", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "great-bear-rainforest", + "ref": "cultural-immersion-with-first-nations-communities", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_cultural-immersion-with-first-nations-communities.jpg" + }, + { + "name": "Hot Springs Relaxation", + "description": "Escape to serenity with a visit to the natural hot springs nestled within the rainforest. Immerse yourself in the warm, mineral-rich waters while surrounded by lush greenery and the soothing sounds of nature. This rejuvenating experience is perfect for relaxation and unwinding after a day of exploring the rainforest.", + "locationName": "Various locations within the rainforest", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "great-bear-rainforest", + "ref": "hot-springs-relaxation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_hot-springs-relaxation.jpg" + }, + { + "name": "Bear Viewing from a Floating Lodge", + "description": "Experience the thrill of observing grizzly bears in their natural habitat from the comfort and safety of a floating lodge. These unique accommodations provide a front-row seat to witness the majestic creatures as they fish for salmon and interact with their surroundings. Expert guides will offer insights into bear behavior and the delicate ecosystem of the rainforest.", + "locationName": "Various lodges within the rainforest", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "great-bear-rainforest", + "ref": "bear-viewing-from-a-floating-lodge", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_bear-viewing-from-a-floating-lodge.jpg" + }, + { + "name": "Scenic Boat Tour", + "description": "Embark on a scenic boat tour through the breathtaking fjords and channels of the Great Bear Rainforest. Witness towering waterfalls cascading down moss-covered cliffs, spot playful seals and dolphins, and marvel at the dense forests that line the shores. This leisurely excursion offers a unique perspective of the rainforest's vastness and beauty.", + "locationName": "Various departure points within the rainforest", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-bear-rainforest", + "ref": "scenic-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_scenic-boat-tour.jpg" + }, + { + "name": "Wildlife Photography Safari", + "description": "Capture the magic of the Great Bear Rainforest with a wildlife photography safari. Accompanied by experienced guides, venture into prime locations to photograph grizzly bears, black bears, wolves, and other fascinating creatures. Learn valuable tips and techniques to enhance your photography skills and create lasting memories of your rainforest adventure.", + "locationName": "Various locations within the rainforest", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "great-bear-rainforest", + "ref": "wildlife-photography-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_wildlife-photography-safari.jpg" + }, + { + "name": "Cultural Immersion with Coastal First Nations", + "description": "Engage in a cultural immersion experience with the Coastal First Nations communities who have inhabited the Great Bear Rainforest for centuries. Learn about their rich traditions, storytelling, art, and deep connection to the land. Participate in workshops, enjoy traditional meals, and gain a profound understanding of their way of life.", + "locationName": "Coastal First Nations villages", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "great-bear-rainforest", + "ref": "cultural-immersion-with-coastal-first-nations", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/great-bear-rainforest_cultural-immersion-with-coastal-first-nations.jpg" + }, + { + "name": "Explore the Ruins of Akrotiri on Santorini", + "description": "Step back in time and wander through the ancient ruins of Akrotiri, a Minoan Bronze Age settlement that was remarkably preserved by volcanic ash. Marvel at the advanced architecture, intricate frescoes, and everyday objects that offer a glimpse into life thousands of years ago. This archaeological wonder is often compared to Pompeii and provides a fascinating journey into the past.", + "locationName": "Akrotiri Archaeological Site, Santorini", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "greek-islands", + "ref": "explore-the-ruins-of-akrotiri-on-santorini", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_explore-the-ruins-of-akrotiri-on-santorini.jpg" + }, + { + "name": "Sail the Aegean Sea", + "description": "Embark on a sailing adventure through the turquoise waters of the Aegean Sea. Feel the refreshing sea breeze as you glide past picturesque islands, secluded coves, and volcanic landscapes. Many tours offer opportunities for swimming, snorkeling, and exploring hidden beaches, allowing you to experience the beauty of the Greek Islands from a unique perspective.", + "locationName": "Various islands and harbors", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "greek-islands", + "ref": "sail-the-aegean-sea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_sail-the-aegean-sea.jpg" + }, + { + "name": "Hike to the Peak of Mount Zas on Naxos", + "description": "For breathtaking panoramic views and a touch of Greek mythology, hike to the summit of Mount Zas, the highest peak in the Cyclades islands. According to legend, this mountain was the childhood home of Zeus, the king of the gods. The trail offers a challenging but rewarding experience, with stunning vistas of the surrounding islands and coastline.", + "locationName": "Mount Zas, Naxos", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "greek-islands", + "ref": "hike-to-the-peak-of-mount-zas-on-naxos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_hike-to-the-peak-of-mount-zas-on-naxos.jpg" + }, + { + "name": "Indulge in a Traditional Greek Feast", + "description": "No trip to the Greek Islands is complete without savoring the delicious local cuisine. Find a charming taverna with outdoor seating and treat your taste buds to a feast of fresh seafood, grilled meats, flavorful cheeses, and vibrant salads. Don't forget to finish your meal with a glass of ouzo or a slice of baklava for a truly authentic experience.", + "locationName": "Various tavernas and restaurants throughout the islands", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "greek-islands", + "ref": "indulge-in-a-traditional-greek-feast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_indulge-in-a-traditional-greek-feast.jpg" + }, + { + "name": "Relax on the iconic Beaches of Mykonos", + "description": "Mykonos is renowned for its stunning beaches, each with its own unique vibe. From the lively Paradise Beach with its beach clubs and water sports to the more secluded Agios Sostis, known for its natural beauty, there's a perfect spot for everyone to unwind and soak up the Mediterranean sun. Enjoy swimming in the crystal-clear waters, trying out water sports, or simply relaxing on the sand with a refreshing drink.", + "locationName": "Various beaches in Mykonos", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "greek-islands", + "ref": "relax-on-the-iconic-beaches-of-mykonos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_relax-on-the-iconic-beaches-of-mykonos.jpg" + }, + { + "name": "Go spelunking in the Melissani Cave", + "description": "Embark on an enchanting boat tour through the Melissani Cave on Kefalonia Island. Glide across the crystal-clear turquoise waters as sunlight streams through the collapsed cave ceiling, illuminating the magical underground lake. Marvel at the unique geological formations and the mystical atmosphere of this natural wonder.", + "locationName": "Melissani Cave, Kefalonia", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "greek-islands", + "ref": "go-spelunking-in-the-melissani-cave", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_go-spelunking-in-the-melissani-cave.jpg" + }, + { + "name": "Hike the Samaria Gorge", + "description": "Challenge yourself with a thrilling hike through the Samaria Gorge on Crete, the longest gorge in Europe. Trek through stunning landscapes, passing through ancient forests, towering cliffs, and refreshing streams. Keep an eye out for the elusive kri-kri, the wild goats that inhabit the gorge.", + "locationName": "Samaria Gorge, Crete", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "greek-islands", + "ref": "hike-the-samaria-gorge", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_hike-the-samaria-gorge.jpg" + }, + { + "name": "Discover the ancient city of Rhodes", + "description": "Step back in time and explore the medieval city of Rhodes, a UNESCO World Heritage Site. Wander through the cobblestone streets of the Old Town, visit the Palace of the Grand Master, and admire the impressive fortifications. Don't miss the Street of the Knights, lined with medieval inns and the Archaeological Museum.", + "locationName": "Rhodes Town, Rhodes", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "greek-islands", + "ref": "discover-the-ancient-city-of-rhodes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_discover-the-ancient-city-of-rhodes.jpg" + }, + { + "name": "Scuba dive or snorkel in the Aegean Sea", + "description": "Plunge into the crystal-clear waters of the Aegean Sea and discover a vibrant underwater world. Explore colorful coral reefs, encounter diverse marine life, and discover ancient shipwrecks. Whether you're an experienced diver or a beginner snorkeler, the Greek Islands offer unforgettable underwater adventures.", + "locationName": "Various islands", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "greek-islands", + "ref": "scuba-dive-or-snorkel-in-the-aegean-sea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_scuba-dive-or-snorkel-in-the-aegean-sea.jpg" + }, + { + "name": "Experience the nightlife of Mykonos", + "description": "Dance the night away in the vibrant nightlife scene of Mykonos. Explore the numerous bars and clubs, from beachfront parties to chic cocktail lounges. Enjoy live music, DJs, and a lively atmosphere that lasts until dawn.", + "locationName": "Mykonos Town, Mykonos", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "greek-islands", + "ref": "experience-the-nightlife-of-mykonos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_experience-the-nightlife-of-mykonos.jpg" + }, + { + "name": "Kayaking to Sea Caves and Hidden Beaches", + "description": "Embark on a kayaking adventure along the dramatic coastlines of islands like Milos or Kefalonia. Paddle into secluded sea caves, discover hidden beaches inaccessible by land, and snorkel in crystal-clear waters. This active experience allows you to explore the islands' natural beauty from a unique perspective.", + "locationName": "Milos or Kefalonia", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "greek-islands", + "ref": "kayaking-to-sea-caves-and-hidden-beaches", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_kayaking-to-sea-caves-and-hidden-beaches.jpg" + }, + { + "name": "Visit a Traditional Village and Learn Local Crafts", + "description": "Step back in time and immerse yourself in the authentic charm of a traditional Greek village. Explore the narrow streets, visit local shops selling handmade crafts, and perhaps even participate in a workshop to learn skills like pottery, weaving, or cheese-making. This cultural experience provides a glimpse into the islands' rich heritage.", + "locationName": "Various Islands", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "greek-islands", + "ref": "visit-a-traditional-village-and-learn-local-crafts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_visit-a-traditional-village-and-learn-local-crafts.jpg" + }, + { + "name": "Wine Tasting at a Vineyard with Stunning Views", + "description": "Indulge in the unique flavors of Greek wine with a visit to a local vineyard. Sample a variety of wines, learn about the winemaking process, and savor breathtaking views of the surrounding landscapes. Santorini and Crete are particularly renowned for their volcanic wines and picturesque vineyards.", + "locationName": "Santorini or Crete", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "greek-islands", + "ref": "wine-tasting-at-a-vineyard-with-stunning-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_wine-tasting-at-a-vineyard-with-stunning-views.jpg" + }, + { + "name": "Sunset Cruise with Dinner and Live Music", + "description": "Experience the magic of a Greek sunset on a romantic cruise along the coast. Enjoy a delicious dinner onboard, sip on cocktails, and be serenaded by live music as you witness the sky ablaze with colors. This unforgettable experience is perfect for couples or those seeking a memorable evening.", + "locationName": "Various Islands", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "greek-islands", + "ref": "sunset-cruise-with-dinner-and-live-music", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_sunset-cruise-with-dinner-and-live-music.jpg" + }, + { + "name": "Hiking or Biking Through Scenic Trails", + "description": "Explore the diverse landscapes of the islands on foot or by bike. Hike through olive groves, pine forests, and along coastal paths, enjoying panoramic views and discovering hidden gems. Many islands offer well-maintained trails suitable for different fitness levels, making it a perfect activity for nature enthusiasts.", + "locationName": "Various Islands", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "greek-islands", + "ref": "hiking-or-biking-through-scenic-trails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_hiking-or-biking-through-scenic-trails.jpg" + }, + { + "name": "Island Hopping Adventure", + "description": "Embark on a multi-day island hopping adventure, exploring the diverse landscapes and unique charm of each island. Ferry between popular destinations like Mykonos, Santorini, and Crete, or venture off the beaten path to discover hidden gems like Folegandros and Milos. Each island offers its own blend of history, culture, and natural beauty, allowing you to customize your itinerary to match your interests.", + "locationName": "Various Greek Islands", + "duration": 48, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "greek-islands", + "ref": "island-hopping-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_island-hopping-adventure.jpg" + }, + { + "name": "Cooking Class with a Local Family", + "description": "Immerse yourself in Greek culture by participating in a cooking class hosted by a local family. Learn the secrets of traditional Greek cuisine, from fresh ingredients and time-honored recipes to the warmth of Greek hospitality. Enjoy the fruits (and vegetables) of your labor with a delicious home-cooked meal shared with your hosts.", + "locationName": "Various Islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "greek-islands", + "ref": "cooking-class-with-a-local-family", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_cooking-class-with-a-local-family.jpg" + }, + { + "name": "Volcanic Exploration on Nisyros", + "description": "Venture to the volcanic island of Nisyros and descend into the crater of an active volcano. Witness the raw power of nature as you explore the lunar-like landscape, feeling the heat beneath your feet and smelling the sulfur in the air. Learn about the island's volcanic history and unique ecosystem.", + "locationName": "Nisyros", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "greek-islands", + "ref": "volcanic-exploration-on-nisyros", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_volcanic-exploration-on-nisyros.jpg" + }, + { + "name": "Stargazing on a Secluded Beach", + "description": "Escape the city lights and find a secluded beach to experience the magic of the Greek night sky. With minimal light pollution, the stars shine brightly, offering a breathtaking view of the Milky Way and constellations. Bring a blanket and some local wine for a truly romantic and unforgettable evening.", + "locationName": "Various Islands", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "greek-islands", + "ref": "stargazing-on-a-secluded-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_stargazing-on-a-secluded-beach.jpg" + }, + { + "name": "Watersports Extravaganza", + "description": "Get your adrenaline pumping with a day of exhilarating watersports. Choose from a variety of options such as windsurfing, kitesurfing, jet skiing, or parasailing. Many islands offer rentals and lessons, allowing you to experience the thrill of riding the waves or soaring above the Aegean Sea.", + "locationName": "Various Islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "greek-islands", + "ref": "watersports-extravaganza", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greek-islands_watersports-extravaganza.jpg" + }, + { + "name": "Boat Tour of the Icefjord", + "description": "Embark on an unforgettable boat tour through the Ilulissat Icefjord, a UNESCO World Heritage site. Marvel at the towering icebergs, some as tall as skyscrapers, that have calved from the Sermeq Kujalleq glacier. Capture stunning photographs of the icy landscape and witness the raw power of nature.", + "locationName": "Ilulissat Icefjord", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "greenland", + "ref": "boat-tour-of-the-icefjord", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_boat-tour-of-the-icefjord.jpg" + }, + { + "name": "Hiking to the Icefjord Viewpoint", + "description": "Lace up your hiking boots and embark on a scenic trail that leads to a breathtaking viewpoint overlooking the Ilulissat Icefjord. Enjoy panoramic views of the massive icebergs and the vast expanse of the Arctic landscape. This moderate hike is suitable for those with a reasonable level of fitness.", + "locationName": "Ilulissat Icefjord", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "greenland", + "ref": "hiking-to-the-icefjord-viewpoint", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_hiking-to-the-icefjord-viewpoint.jpg" + }, + { + "name": "Kayaking Among Icebergs", + "description": "Experience the thrill of kayaking amidst the icebergs of the Ilulissat Icefjord. Paddle through the calm waters, surrounded by the towering ice formations, and feel a sense of awe and wonder. This adventure activity is suitable for experienced kayakers.", + "locationName": "Ilulissat Icefjord", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "greenland", + "ref": "kayaking-among-icebergs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_kayaking-among-icebergs.jpg" + }, + { + "name": "Visit the Ilulissat Museum", + "description": "Delve into the history and culture of Greenland at the Ilulissat Museum. Learn about the Inuit people, their traditions, and their way of life in this Arctic region. Explore exhibits on the geology of the icefjord and the impact of climate change.", + "locationName": "Ilulissat Town", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "greenland", + "ref": "visit-the-ilulissat-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_visit-the-ilulissat-museum.jpg" + }, + { + "name": "Dog Sledding Adventure", + "description": "Embark on a thrilling dog sledding adventure across the frozen landscape. Experience the traditional mode of transportation of the Inuit people and enjoy the exhilaration of being pulled by a team of huskies. This activity is available during the winter months.", + "locationName": "Ilulissat Icefjord", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "greenland", + "ref": "dog-sledding-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_dog-sledding-adventure.jpg" + }, + { + "name": "Northern Lights Viewing", + "description": "Experience the magic of the Aurora Borealis dancing across the Arctic sky. Join a guided evening tour to escape the city lights and witness this breathtaking natural phenomenon. Warm beverages and local folklore add to the enchantment of this unforgettable experience.", + "locationName": "Ilulissat Icefjord and surrounding areas", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "greenland", + "ref": "northern-lights-viewing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_northern-lights-viewing.jpg" + }, + { + "name": "Whale Watching Excursion", + "description": "Embark on a thrilling boat trip in search of majestic whales that inhabit the waters around Ilulissat Icefjord. Witness humpback whales, minke whales, and even fin whales breaching and spouting in their natural habitat. Knowledgeable guides provide insights into these fascinating creatures and the delicate Arctic ecosystem.", + "locationName": "Disko Bay", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "greenland", + "ref": "whale-watching-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_whale-watching-excursion.jpg" + }, + { + "name": "Sermermiut Eskimo Settlement Hike", + "description": "Step back in time with a hike to the abandoned Sermermiut settlement. Explore the remains of ancient Inuit dwellings and learn about the history and culture of the people who thrived in this harsh Arctic environment for centuries. The panoramic views of the Icefjord add to the allure of this historical and scenic adventure.", + "locationName": "Sermermiut", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "greenland", + "ref": "sermermiut-eskimo-settlement-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_sermermiut-eskimo-settlement-hike.jpg" + }, + { + "name": "Greenlandic Gastronomy Experience", + "description": "Indulge in the unique flavors of Greenlandic cuisine. Savor traditional dishes like suaasat (seal soup), musk ox steak, and fresh Arctic seafood. Local restaurants and cafes offer a delightful culinary journey, showcasing the region's rich food culture and reliance on seasonal ingredients.", + "locationName": "Ilulissat town", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "greenland", + "ref": "greenlandic-gastronomy-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_greenlandic-gastronomy-experience.jpg" + }, + { + "name": "Arctic Wildlife Photography Tour", + "description": "Capture the beauty of Greenland's wildlife through the lens of your camera. Join a photography tour led by experienced guides who will take you to prime locations for spotting reindeer, Arctic foxes, seabirds, and other fascinating creatures. Learn valuable photography tips and techniques while immersing yourself in the stunning Arctic landscapes.", + "locationName": "Ilulissat Icefjord and surrounding areas", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "greenland", + "ref": "arctic-wildlife-photography-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_arctic-wildlife-photography-tour.jpg" + }, + { + "name": "Midnight Sun Hike", + "description": "Experience the surreal phenomenon of the midnight sun, where the sun remains visible even at night, by embarking on a guided hiking tour. Witness the ethereal glow of the sun casting long shadows on the icebergs and the surrounding landscape, creating a truly magical experience. This unique opportunity allows you to explore the Arctic wilderness under the soft light of the midnight sun, capturing breathtaking photos and creating unforgettable memories.", + "locationName": "Sermermiut Valley", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "greenland", + "ref": "midnight-sun-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_midnight-sun-hike.jpg" + }, + { + "name": "Ice Climbing Adventure", + "description": "For thrill-seekers, embark on an ice climbing adventure on the frozen waterfalls and cliffs surrounding the Ilulissat Icefjord. With experienced guides and proper equipment, challenge yourself with this exhilarating activity, testing your strength and agility as you ascend the icy surfaces. Enjoy breathtaking views of the icefjord and surrounding landscapes from unique vantage points.", + "locationName": "Various locations around the Ilulissat Icefjord", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "greenland", + "ref": "ice-climbing-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_ice-climbing-adventure.jpg" + }, + { + "name": "Greenlandic Culture Workshop", + "description": "Immerse yourself in the rich culture of Greenland by participating in a Greenlandic culture workshop. Learn about traditional crafts such as drum dancing, mask making, and Inuit storytelling. Engage with local artisans and gain insights into their way of life, traditions, and connection to the Arctic environment.", + "locationName": "Ilulissat Community Center", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "greenland", + "ref": "greenlandic-culture-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_greenlandic-culture-workshop.jpg" + }, + { + "name": "Arctic Wildlife Safari", + "description": "Embark on an Arctic wildlife safari by boat or jeep to explore the diverse fauna of the region. Keep an eye out for reindeer, Arctic foxes, musk oxen, and various bird species that inhabit the area. Learn about their adaptations to the harsh Arctic environment and their role in the ecosystem from knowledgeable guides.", + "locationName": "Tundra areas around Ilulissat Icefjord", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "greenland", + "ref": "arctic-wildlife-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_arctic-wildlife-safari.jpg" + }, + { + "name": "Kayak Fishing Expedition", + "description": "Combine the thrill of kayaking with the excitement of fishing on a kayak fishing expedition. Paddle through the calm waters of the Ilulissat Icefjord, surrounded by towering icebergs, and try your luck at catching Arctic fish species like halibut or cod. Enjoy the tranquility of the surroundings and the satisfaction of reeling in your own catch.", + "locationName": "Ilulissat Icefjord", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "greenland", + "ref": "kayak-fishing-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_kayak-fishing-expedition.jpg" + }, + { + "name": "Snowshoeing in the Arctic Wilderness", + "description": "Embark on a serene snowshoeing adventure through the pristine Arctic wilderness surrounding the Ilulissat Icefjord. Explore the snow-covered landscapes, breathe in the crisp air, and experience the tranquility of the Arctic winter. This activity offers a unique way to connect with nature and enjoy the peaceful beauty of Greenland.", + "locationName": "Ilulissat Icefjord surrounding area", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "greenland", + "ref": "snowshoeing-in-the-arctic-wilderness", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_snowshoeing-in-the-arctic-wilderness.jpg" + }, + { + "name": "Scenic Flight over the Icefjord", + "description": "Take to the skies for a breathtaking scenic flight over the Ilulissat Icefjord and witness the grandeur of the icebergs from a different perspective. Soar above the frozen landscape, marvel at the vastness of the icefjord, and capture stunning aerial photographs of this natural wonder.", + "locationName": "Ilulissat Airport", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "greenland", + "ref": "scenic-flight-over-the-icefjord", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_scenic-flight-over-the-icefjord.jpg" + }, + { + "name": "Visit the Knud Rasmussen Museum", + "description": "Delve into the history of Greenland and the Arctic at the Knud Rasmussen Museum. Learn about the famous polar explorer Knud Rasmussen, explore exhibits on Inuit culture and history, and gain insights into the region's unique heritage and way of life.", + "locationName": "Ilulissat town", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "greenland", + "ref": "visit-the-knud-rasmussen-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_visit-the-knud-rasmussen-museum.jpg" + }, + { + "name": "Greenlandic Kaffemik Experience", + "description": "Immerse yourself in Greenlandic culture by attending a traditional Kaffemik gathering. Enjoy coffee, tea, and local delicacies while socializing with local residents and learning about their customs and traditions. This is a wonderful opportunity to experience Greenlandic hospitality and connect with the community.", + "locationName": "Local homes in Ilulissat", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "greenland", + "ref": "greenlandic-kaffemik-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_greenlandic-kaffemik-experience.jpg" + }, + { + "name": "Stargazing in the Arctic Night", + "description": "Experience the magic of the Arctic night sky with a stargazing excursion. Away from the city lights, marvel at the celestial wonders, including the possibility of witnessing the mesmerizing Northern Lights. Learn about constellations and astronomical phenomena from expert guides.", + "locationName": "Ilulissat Icefjord surrounding area", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "greenland", + "ref": "stargazing-in-the-arctic-night", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/greenland_stargazing-in-the-arctic-night.jpg" + }, + { + "name": "Hike to Carbet Falls", + "description": "Embark on an invigorating hike through the lush rainforest to witness the breathtaking Carbet Falls, a series of three cascading waterfalls nestled amidst volcanic slopes. The trail offers varying difficulty levels, catering to both casual hikers and seasoned adventurers. Be prepared to be mesmerized by the power and beauty of nature as you immerse yourself in the sights and sounds of the rainforest.", + "locationName": "Carbet Falls", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "guadeloupe", + "ref": "hike-to-carbet-falls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_hike-to-carbet-falls.jpg" + }, + { + "name": "Discover the Charm of Pointe-à-Pitre", + "description": "Wander through the colorful streets of Pointe-à-Pitre, Guadeloupe's largest city, and soak up the vibrant atmosphere. Explore the bustling markets, admire the colonial architecture, and visit historical landmarks such as the Saint-Pierre and Saint-Paul Cathedral. Don't miss the opportunity to savor delicious Creole cuisine at local restaurants and experience the unique blend of French and Caribbean culture.", + "locationName": "Pointe-à-Pitre", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "guadeloupe", + "ref": "discover-the-charm-of-pointe-pitre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_discover-the-charm-of-pointe-pitre.jpg" + }, + { + "name": "Relax on Grande Anse Beach", + "description": "Unwind on the pristine sands of Grande Anse Beach, considered one of the most beautiful beaches in Guadeloupe. Bask in the warm Caribbean sun, take a refreshing dip in the turquoise waters, or simply enjoy the breathtaking views of the coastline. With its calm waters and gentle waves, it's an ideal spot for families with children and those seeking a tranquil escape.", + "locationName": "Grande Anse Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "guadeloupe", + "ref": "relax-on-grande-anse-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_relax-on-grande-anse-beach.jpg" + }, + { + "name": "Visit La Soufrière Volcano", + "description": "Embark on a thrilling adventure to La Soufrière, an active volcano and the highest peak in the Lesser Antilles. Hike through volcanic landscapes, witness steaming fumaroles, and enjoy panoramic views from the summit. This experience is perfect for adventure seekers and those interested in the geological wonders of the island.", + "locationName": "La Soufrière Volcano", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "guadeloupe", + "ref": "visit-la-soufri-re-volcano", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_visit-la-soufri-re-volcano.jpg" + }, + { + "name": "Island Hopping Adventure", + "description": "Embark on a boat tour to explore the surrounding islands of Guadeloupe, each offering its own unique charm. Discover the pristine beaches of Petite Terre, snorkel in the turquoise waters of Marie-Galante, or immerse yourself in the history of Les Saintes. This island-hopping adventure promises breathtaking scenery and unforgettable memories.", + "locationName": "Guadeloupe Archipelago", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "guadeloupe", + "ref": "island-hopping-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_island-hopping-adventure.jpg" + }, + { + "name": "Kayaking through the Mangroves", + "description": "Paddle through the tranquil mangrove forests of Grand Cul-de-Sac Marin, a protected natural reserve. Observe the diverse ecosystem, including exotic birds, fish, and crabs, while enjoying the serenity of this unique environment. Kayaking tours offer a peaceful way to connect with nature and explore Guadeloupe's hidden gems.", + "locationName": "Grand Cul-de-Sac Marin", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "guadeloupe", + "ref": "kayaking-through-the-mangroves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_kayaking-through-the-mangroves.jpg" + }, + { + "name": "Indulge in Creole Cuisine", + "description": "Embark on a culinary journey through Guadeloupe's vibrant food scene. Savor the unique flavors of Creole cuisine, a fusion of French, African, and Indian influences. Sample local delicacies such as accras (cod fritters), colombo (curried meat or vegetables), and bokit (stuffed sandwich) at traditional restaurants or bustling markets.", + "locationName": "Various Restaurants and Markets", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "guadeloupe", + "ref": "indulge-in-creole-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_indulge-in-creole-cuisine.jpg" + }, + { + "name": "Sunset Catamaran Cruise", + "description": "Set sail on a romantic catamaran cruise as the sun dips below the horizon. Enjoy breathtaking views of the coastline, sip on cocktails, and sway to the rhythm of Caribbean music. This unforgettable experience is perfect for couples or anyone seeking a magical evening on the water.", + "locationName": "Guadeloupe Coastline", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "guadeloupe", + "ref": "sunset-catamaran-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_sunset-catamaran-cruise.jpg" + }, + { + "name": "Discover the History of Fort Delgrès", + "description": "Step back in time at Fort Delgrès, a historic landmark overlooking Basse-Terre. Explore the ruins of this 17th-century fort, which played a significant role in Guadeloupe's colonial past. Learn about the island's history and enjoy panoramic views of the surrounding landscape.", + "locationName": "Fort Delgrès", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "guadeloupe", + "ref": "discover-the-history-of-fort-delgr-s", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_discover-the-history-of-fort-delgr-s.jpg" + }, + { + "name": "Canyoning Adventure in the Jungle", + "description": "Embark on an exhilarating canyoning adventure through Guadeloupe's lush rainforests. Rappel down cascading waterfalls, slide down natural water slides, and jump into refreshing pools. This adrenaline-pumping activity is perfect for thrill-seekers and nature lovers.", + "locationName": "Guadeloupe National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "guadeloupe", + "ref": "canyoning-adventure-in-the-jungle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_canyoning-adventure-in-the-jungle.jpg" + }, + { + "name": "Horseback Riding on the Beach", + "description": "Experience the beauty of Guadeloupe's coastline on horseback. Ride along pristine beaches, through coconut groves, and enjoy breathtaking ocean views. This activity is suitable for all skill levels and offers a unique perspective of the island.", + "locationName": "Sainte-Anne Beach", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "guadeloupe", + "ref": "horseback-riding-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_horseback-riding-on-the-beach.jpg" + }, + { + "name": "Rum Distillery Tour and Tasting", + "description": "Delve into the rich history of rum production in Guadeloupe. Visit a traditional distillery, learn about the rum-making process, and indulge in a tasting of various aged rums. This cultural experience is perfect for those who appreciate fine spirits.", + "locationName": "Damoiseau Distillery", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "guadeloupe", + "ref": "rum-distillery-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_rum-distillery-tour-and-tasting.jpg" + }, + { + "name": "Stargazing on the Beach", + "description": "Escape the city lights and marvel at the celestial wonders above. Join a guided stargazing tour on a secluded beach, where you can observe constellations, planets, and the Milky Way with the help of telescopes and expert astronomers.", + "locationName": "Plage de la Caravelle", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "guadeloupe", + "ref": "stargazing-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_stargazing-on-the-beach.jpg" + }, + { + "name": "Scuba Diving at Pigeon Island", + "description": "Explore the vibrant underwater world of Pigeon Island, a protected marine reserve renowned for its diverse marine life. Discover colorful coral reefs, swim alongside tropical fish, and encounter fascinating sea creatures such as sea turtles and rays. This activity is perfect for certified scuba divers.", + "locationName": "Pigeon Island", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "guadeloupe", + "ref": "scuba-diving-at-pigeon-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_scuba-diving-at-pigeon-island.jpg" + }, + { + "name": "Ziplining through the Rainforest Canopy", + "description": "Experience the thrill of soaring through the lush rainforest canopy on a zipline adventure. Enjoy breathtaking views of the island's diverse flora and fauna as you glide from platform to platform, feeling the adrenaline rush of this exhilarating activity. Several locations across Basse-Terre offer zipline courses suitable for various ages and skill levels.", + "locationName": "Rainforest Canopy", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "guadeloupe", + "ref": "ziplining-through-the-rainforest-canopy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_ziplining-through-the-rainforest-canopy.jpg" + }, + { + "name": "Explore the Local Markets", + "description": "Immerse yourself in the vibrant culture of Guadeloupe by visiting the bustling local markets. Discover a variety of fresh produce, spices, handcrafted souvenirs, and local delicacies. Engage with friendly vendors, practice your French or Creole, and experience the authentic charm of Guadeloupean life.", + "locationName": "Pointe-à-Pitre or Basse-Terre", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "guadeloupe", + "ref": "explore-the-local-markets", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_explore-the-local-markets.jpg" + }, + { + "name": "Take a Cooking Class", + "description": "Delve into the world of Creole cuisine by taking a cooking class. Learn the secrets of preparing traditional dishes like Colombo, accras, and poisson cru. Master the art of blending spices and creating flavorful sauces, and enjoy the fruits of your labor with a delicious meal.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "guadeloupe", + "ref": "take-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_take-a-cooking-class.jpg" + }, + { + "name": "Go Birdwatching in the National Park", + "description": "Embark on a birdwatching adventure in Guadeloupe National Park. With over 100 bird species, including the Guadeloupe woodpecker and the white-breasted thrasher, the park offers a haven for bird enthusiasts. Hike through diverse ecosystems, from rainforests to mangroves, and observe these feathered creatures in their natural habitat.", + "locationName": "Guadeloupe National Park", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "guadeloupe", + "ref": "go-birdwatching-in-the-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_go-birdwatching-in-the-national-park.jpg" + }, + { + "name": "Enjoy Watersports", + "description": "Embrace the crystal-clear waters of Guadeloupe with a variety of watersports. Try windsurfing or kitesurfing, rent a jet ski for an adrenaline rush, or go stand-up paddleboarding for a more relaxing experience. The calm lagoons and steady trade winds provide ideal conditions for beginners and experienced enthusiasts.", + "locationName": "Various beaches", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "guadeloupe", + "ref": "enjoy-watersports", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guadeloupe_enjoy-watersports.jpg" + }, + { + "name": "Kayaking on the Lake", + "description": "Embark on a serene kayaking adventure across the crystal-clear waters of Lake Atitlán. Witness breathtaking panoramic views of the surrounding volcanoes and charming villages while enjoying the tranquility of the lake. Rent a kayak and explore at your own pace, or join a guided tour to discover hidden coves and learn about the area's rich history and ecosystem.", + "locationName": "Lake Atitlán", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "guatemala", + "ref": "kayaking-on-the-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_kayaking-on-the-lake.jpg" + }, + { + "name": "Hiking Indian Nose", + "description": "Challenge yourself with a hike up Indian Nose, a mountain peak known for its unique profile resembling a person's face. The trail offers stunning vistas of the lake and surrounding landscapes, especially during sunrise when the golden light bathes the scenery. Be prepared for a moderate climb and pack water and snacks for the journey.", + "locationName": "Indian Nose", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "guatemala", + "ref": "hiking-indian-nose", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_hiking-indian-nose.jpg" + }, + { + "name": "Exploring Mayan Villages", + "description": "Immerse yourself in the vibrant Mayan culture by visiting the traditional villages that dot the shores of Lake Atitlán. Each village has its unique charm, from the bustling market of Santiago Atitlán to the artistic hub of San Juan la Laguna. Interact with local artisans, witness traditional weaving techniques, and learn about their customs and beliefs.", + "locationName": "Various villages around Lake Atitlán", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "guatemala", + "ref": "exploring-mayan-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_exploring-mayan-villages.jpg" + }, + { + "name": "Coffee Plantation Tour", + "description": "Delve into the world of Guatemalan coffee with a tour of a local coffee plantation. Learn about the coffee-making process, from bean to cup, and discover the unique flavors and aromas of the region's renowned coffee beans. Enjoy a tasting session and appreciate the meticulous care that goes into each brew.", + "locationName": "Various coffee plantations around Lake Atitlán", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "guatemala", + "ref": "coffee-plantation-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_coffee-plantation-tour.jpg" + }, + { + "name": "Sunset Cruise", + "description": "Conclude your day with a magical sunset cruise on Lake Atitlán. Witness the sky ablaze with vibrant colors as the sun dips below the horizon, casting long shadows over the water and surrounding volcanoes. Enjoy the peaceful ambiance and capture unforgettable moments of this breathtaking spectacle.", + "locationName": "Lake Atitlán", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "guatemala", + "ref": "sunset-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_sunset-cruise.jpg" + }, + { + "name": "Scuba Diving in Lake Atitlán", + "description": "Explore the underwater world of Lake Atitlán, including unique volcanic formations, submerged Mayan ruins, and diverse fish species. Several dive shops around the lake offer guided dives for all skill levels, allowing you to discover the hidden depths of this volcanic lake.", + "locationName": "Lake Atitlán", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "guatemala", + "ref": "scuba-diving-in-lake-atitl-n", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_scuba-diving-in-lake-atitl-n.jpg" + }, + { + "name": "Paragliding over Lake Atitlán", + "description": "Soar above the stunning landscapes of Lake Atitlán on a tandem paragliding flight. Experience breathtaking aerial views of the volcanic caldera, surrounding villages, and the glistening lake. This thrilling adventure offers a unique perspective and unforgettable memories.", + "locationName": "Lake Atitlán", + "duration": 1, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "guatemala", + "ref": "paragliding-over-lake-atitl-n", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_paragliding-over-lake-atitl-n.jpg" + }, + { + "name": "Visit the Chichicastenango Market", + "description": "Immerse yourself in the vibrant atmosphere of the Chichicastenango Market, one of the largest and most colorful markets in Central America. Browse through a vast array of handicrafts, textiles, ceramics, and local produce, while experiencing the cultural richness of the indigenous Mayan people.", + "locationName": "Chichicastenango", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "guatemala", + "ref": "visit-the-chichicastenango-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_visit-the-chichicastenango-market.jpg" + }, + { + "name": "Spanish Immersion and Homestay", + "description": "Combine language learning with cultural immersion through a Spanish immersion program and homestay experience. Live with a local family, attend language classes, and practice your Spanish while engaging in daily life and cultural activities. This is a fantastic way to gain language skills and a deeper understanding of Guatemalan culture.", + "locationName": "Lake Atitlán", + "duration": 14, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "guatemala", + "ref": "spanish-immersion-and-homestay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_spanish-immersion-and-homestay.jpg" + }, + { + "name": "Relax at a lakeside spa", + "description": "Unwind and rejuvenate at one of the many lakeside spas offering a variety of treatments, including massages, facials, and body wraps. Enjoy stunning lake views while indulging in relaxation and self-care, providing the perfect escape from the everyday hustle and bustle.", + "locationName": "Lake Atitlán", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "guatemala", + "ref": "relax-at-a-lakeside-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_relax-at-a-lakeside-spa.jpg" + }, + { + "name": "Birdwatching at the Atitlán Nature Reserve", + "description": "Embark on a serene journey through the Atitlán Nature Reserve, a sanctuary for diverse bird species. Hike through lush trails, accompanied by expert guides, and spot vibrant feathered creatures like the azure-rumped tanager and pink-headed warbler. Immerse yourself in the tranquil sounds of nature and capture stunning photos of these avian wonders.", + "locationName": "Atitlán Nature Reserve", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "guatemala", + "ref": "birdwatching-at-the-atitl-n-nature-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_birdwatching-at-the-atitl-n-nature-reserve.jpg" + }, + { + "name": "Mountain Biking Adventure", + "description": "Experience the thrill of mountain biking through the rugged landscapes surrounding Lake Atitlán. Rent a bike and explore scenic trails with varying difficulty levels, offering breathtaking views of the lake and volcanoes. Whether you're a seasoned rider or a beginner, this adventure will get your adrenaline pumping and provide a unique perspective of the region.", + "locationName": "Various trails around Lake Atitlán", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "guatemala", + "ref": "mountain-biking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_mountain-biking-adventure.jpg" + }, + { + "name": "Weaving Workshop with Local Artisans", + "description": "Immerse yourself in the rich textile traditions of Guatemala with a weaving workshop. Learn from skilled Mayan artisans in their homes or workshops, gaining insights into the intricate techniques and cultural significance of backstrap loom weaving. Create your own unique textile souvenir and support local communities.", + "locationName": "Villages around Lake Atitlán", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "guatemala", + "ref": "weaving-workshop-with-local-artisans", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_weaving-workshop-with-local-artisans.jpg" + }, + { + "name": "Stargazing by the Lake", + "description": "Escape the city lights and marvel at the celestial wonders above Lake Atitlán. Join a stargazing tour or simply find a secluded spot on the shore. With minimal light pollution, the night sky explodes with stars, constellations, and even the Milky Way. Relax under the vast expanse and contemplate the universe.", + "locationName": "Lake Atitlán shores", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "guatemala", + "ref": "stargazing-by-the-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_stargazing-by-the-lake.jpg" + }, + { + "name": "Stand Up Paddleboarding on the Lake", + "description": "Glide across the calm waters of Lake Atitlán on a stand-up paddleboard. Rent a board and explore the shoreline at your own pace, enjoying the panoramic views of the surrounding volcanoes and villages. This relaxing activity is perfect for all skill levels and offers a unique way to connect with nature.", + "locationName": "Lake Atitlán", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "guatemala", + "ref": "stand-up-paddleboarding-on-the-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_stand-up-paddleboarding-on-the-lake.jpg" + }, + { + "name": "Rock Climbing at Indian Nose", + "description": "Challenge yourself with a thrilling rock climbing adventure at Indian Nose, a prominent mountain overlooking Lake Atitlán. Experienced guides will lead you on an exhilarating ascent, offering stunning panoramic views of the lake and surrounding volcanoes as you conquer the rocky cliffs. This activity is perfect for adventure seekers looking for an adrenaline rush and a unique perspective of the breathtaking landscape.", + "locationName": "Indian Nose", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "guatemala", + "ref": "rock-climbing-at-indian-nose", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_rock-climbing-at-indian-nose.jpg" + }, + { + "name": "Visit Santiago Atitlán and its Mayan Culture", + "description": "Embark on a cultural journey to Santiago Atitlán, the largest lakeside town known for its strong Mayan heritage. Explore the vibrant local market, where you can find traditional textiles, handicrafts, and fresh produce. Visit the church of St. James the Apostle, a significant religious site, and learn about the town's unique blend of Catholicism and Mayan beliefs. Immerse yourself in the local culture and witness traditional ceremonies or rituals.", + "locationName": "Santiago Atitlán", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "guatemala", + "ref": "visit-santiago-atitl-n-and-its-mayan-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_visit-santiago-atitl-n-and-its-mayan-culture.jpg" + }, + { + "name": "Horseback Riding through the Highlands", + "description": "Experience the beauty of the Guatemalan highlands on horseback. Ride through scenic trails, passing by traditional villages, coffee plantations, and lush forests. Enjoy breathtaking views of the lake and volcanoes as you connect with nature and experience the local way of life. This activity is suitable for all skill levels and offers a unique way to explore the region.", + "locationName": "Lake Atitlán Highlands", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "guatemala", + "ref": "horseback-riding-through-the-highlands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_horseback-riding-through-the-highlands.jpg" + }, + { + "name": "Sample Local Cuisine at a Cooking Class", + "description": "Delve into the flavors of Guatemalan cuisine by participating in a cooking class. Learn to prepare traditional dishes like pepián, a rich stew, or rellenitos, sweet plantains filled with black beans. Discover the secrets of local ingredients and cooking techniques, and savor the delicious results of your culinary creations. This activity is a must for food enthusiasts and provides a deeper understanding of Guatemalan culture.", + "locationName": "Various locations around Lake Atitlán", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "guatemala", + "ref": "sample-local-cuisine-at-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_sample-local-cuisine-at-a-cooking-class.jpg" + }, + { + "name": "Catch a Live Music Performance", + "description": "Immerse yourself in the vibrant nightlife of Lake Atitlán by attending a live music performance. Several bars and restaurants around the lake host local bands and musicians, offering a diverse range of musical styles from traditional Mayan music to contemporary Latin beats. Enjoy a lively atmosphere, sip on local cocktails, and dance the night away under the stars.", + "locationName": "Various bars and restaurants around Lake Atitlán", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "guatemala", + "ref": "catch-a-live-music-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/guatemala_catch-a-live-music-performance.jpg" + }, + { + "name": "Cruise Among the Limestone Islands", + "description": "Embark on a scenic cruise through the emerald waters of Ha Long Bay, marveling at the towering limestone formations and hidden caves. Enjoy a delicious seafood lunch on board as you soak in the breathtaking views of this UNESCO World Heritage Site. Opt for a day trip or an overnight adventure on a traditional junk boat for a truly immersive experience.", + "locationName": "Ha Long Bay", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "ha-long-bay", + "ref": "cruise-among-the-limestone-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_cruise-among-the-limestone-islands.jpg" + }, + { + "name": "Kayaking Adventure", + "description": "Paddle through the tranquil waters of Ha Long Bay on a kayaking excursion, exploring hidden coves, lagoons, and floating villages. Get up close to the limestone karst formations and discover secret beaches. This activity is perfect for those seeking a more active and adventurous way to experience the bay.", + "locationName": "Ha Long Bay", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "kayaking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_kayaking-adventure.jpg" + }, + { + "name": "Visit Sung Sot Cave (Surprise Cave)", + "description": "Explore the magnificent Sung Sot Cave, one of the largest and most impressive caves in Ha Long Bay. Marvel at the stunning stalactites and stalagmites, and learn about the fascinating geological history of the region. This cave offers a unique glimpse into the hidden world beneath the limestone islands.", + "locationName": "Bo Hon Island", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "visit-sung-sot-cave-surprise-cave-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_visit-sung-sot-cave-surprise-cave-.jpg" + }, + { + "name": "Explore Cat Ba Island", + "description": "Venture beyond the bay to Cat Ba Island, the largest island in the Ha Long archipelago. Hike through the lush Cat Ba National Park, home to diverse flora and fauna, including the endangered Cat Ba langur. Visit the Hospital Cave, a historical landmark used during the Vietnam War, or relax on the pristine beaches of Cat Co.", + "locationName": "Cat Ba Island", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "ha-long-bay", + "ref": "explore-cat-ba-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_explore-cat-ba-island.jpg" + }, + { + "name": "Indulge in Local Seafood", + "description": "Savor the freshest seafood at one of the many floating restaurants in Ha Long Bay. Enjoy a variety of dishes, including grilled fish, prawns, and crabs, while taking in the stunning views of the surrounding islands. This is a must-do experience for any foodie visiting the region.", + "locationName": "Ha Long Bay floating restaurants", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "indulge-in-local-seafood", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_indulge-in-local-seafood.jpg" + }, + { + "name": "Rock Climbing and Deep Water Soloing", + "description": "Experience the thrill of scaling Ha Long Bay's limestone cliffs with rock climbing and deep water soloing adventures. Professional guides will lead you on exhilarating climbs, allowing you to conquer challenging routes and enjoy breathtaking views of the bay. For the ultimate adrenaline rush, try deep water soloing, where you climb without ropes over the water, taking a refreshing plunge upon reaching the top.", + "locationName": "Cat Ba Island and Lan Ha Bay", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "ha-long-bay", + "ref": "rock-climbing-and-deep-water-soloing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_rock-climbing-and-deep-water-soloing.jpg" + }, + { + "name": "Floating Village Visit and Cultural Immersion", + "description": "Immerse yourself in the local culture by visiting a floating village in Ha Long Bay. Interact with the friendly residents, learn about their traditional way of life, and witness their unique fishing techniques. You can even try your hand at net fishing or pearl farming. Enjoy a home-cooked meal in a local family's home, savoring authentic Vietnamese flavors and hospitality.", + "locationName": "Cua Van Floating Village or Vung Vieng Fishing Village", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "floating-village-visit-and-cultural-immersion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_floating-village-visit-and-cultural-immersion.jpg" + }, + { + "name": "Sunset Cruise with Squid Fishing", + "description": "Embark on a romantic sunset cruise through the enchanting waters of Ha Long Bay. As the sky transforms into a canvas of vibrant colors, witness the magical beauty of the limestone islands bathed in golden light. Enjoy a delicious seafood dinner on board and try your hand at squid fishing under the starry night sky.", + "locationName": "Ha Long Bay", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "ha-long-bay", + "ref": "sunset-cruise-with-squid-fishing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_sunset-cruise-with-squid-fishing.jpg" + }, + { + "name": "Hiking and Exploring Cat Ba National Park", + "description": "Embark on a hiking adventure through the lush rainforests of Cat Ba National Park. Discover hidden trails, encounter diverse wildlife, and reach panoramic viewpoints offering breathtaking vistas of the bay and surrounding islands. Keep an eye out for the elusive Cat Ba langur, one of the world's most endangered primates.", + "locationName": "Cat Ba National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "hiking-and-exploring-cat-ba-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_hiking-and-exploring-cat-ba-national-park.jpg" + }, + { + "name": "Seaplane or Helicopter Tour", + "description": "Take to the skies for an unforgettable aerial perspective of Ha Long Bay. A seaplane or helicopter tour will provide you with stunning panoramic views of the limestone islands, hidden lagoons, and emerald waters. Capture breathtaking photos and create lasting memories of this unique and awe-inspiring landscape.", + "locationName": "Ha Long Bay", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "ha-long-bay", + "ref": "seaplane-or-helicopter-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_seaplane-or-helicopter-tour.jpg" + }, + { + "name": "Tai Chi on the Sundeck", + "description": "Start your day with a serene Tai Chi session on the sundeck of your cruise ship or at a beachfront resort. As the sun rises over the bay, breathe in the fresh air and move your body in harmony with the gentle flow of Tai Chi, setting the tone for a peaceful and mindful day.", + "locationName": "Cruise ship sundeck or beachfront resort", + "duration": 1, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "tai-chi-on-the-sundeck", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_tai-chi-on-the-sundeck.jpg" + }, + { + "name": "Cooking Class with a Local Chef", + "description": "Immerse yourself in Vietnamese culinary culture by joining a cooking class. Learn to prepare authentic dishes like fresh spring rolls, fragrant pho, or savory seafood specialties under the guidance of a local chef. This hands-on experience will tantalize your taste buds and leave you with newfound culinary skills.", + "locationName": "Local cooking school or restaurant", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "ha-long-bay", + "ref": "cooking-class-with-a-local-chef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_cooking-class-with-a-local-chef.jpg" + }, + { + "name": "Visit a Pearl Farm", + "description": "Discover the fascinating process of pearl cultivation with a visit to a local pearl farm. Learn about the different types of pearls, the meticulous techniques involved in their cultivation, and even witness the delicate art of pearl harvesting. You can also browse exquisite pearl jewelry and find the perfect souvenir to remember your trip.", + "locationName": "Pearl farm on a nearby island", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "visit-a-pearl-farm", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_visit-a-pearl-farm.jpg" + }, + { + "name": "Biking through Cat Ba Island's Countryside", + "description": "Rent a bicycle and embark on a scenic journey through the picturesque countryside of Cat Ba Island. Pedal along quiet roads, passing by rice paddies, traditional villages, and lush greenery. This leisurely activity allows you to experience the island's natural beauty and local life at your own pace.", + "locationName": "Cat Ba Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "ha-long-bay", + "ref": "biking-through-cat-ba-island-s-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_biking-through-cat-ba-island-s-countryside.jpg" + }, + { + "name": "Stargazing on the Bay", + "description": "Escape the city lights and experience the magic of stargazing on the bay. Find a secluded spot on the beach or your cruise ship deck, lie down, and gaze up at the breathtaking canopy of stars. With minimal light pollution, Ha Long Bay offers an incredible opportunity to appreciate the vastness and beauty of the night sky.", + "locationName": "Secluded beach or cruise ship deck", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "ha-long-bay", + "ref": "stargazing-on-the-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_stargazing-on-the-bay.jpg" + }, + { + "name": "Scuba Diving and Snorkeling Adventures", + "description": "Dive into the crystal-clear waters of Ha Long Bay and discover a vibrant underwater world teeming with marine life. Explore coral reefs, encounter colorful fish, and experience the thrill of discovering hidden caves beneath the surface. Whether you're a seasoned diver or a beginner snorkeler, there are options for all levels to enjoy this unforgettable experience.", + "locationName": "Various dive sites around Ha Long Bay", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "ha-long-bay", + "ref": "scuba-diving-and-snorkeling-adventures", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_scuba-diving-and-snorkeling-adventures.jpg" + }, + { + "name": "Visit Cua Van Floating Village", + "description": "Embark on a unique cultural experience by visiting Cua Van, a traditional floating village in Ha Long Bay. Interact with the local community, learn about their way of life, and witness their fishing traditions passed down through generations. You can even try your hand at rowing a traditional bamboo boat and gain insight into this fascinating way of life on the water.", + "locationName": "Cua Van Floating Village", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "visit-cua-van-floating-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_visit-cua-van-floating-village.jpg" + }, + { + "name": "Indulge in Vietnamese Cooking Classes", + "description": "Unleash your inner chef and learn the secrets of authentic Vietnamese cuisine. Join a cooking class led by a local expert and master the art of preparing traditional dishes like fresh spring rolls, fragrant pho, and flavorful stir-fries. Discover the unique blend of herbs, spices, and fresh ingredients that make Vietnamese food so delicious.", + "locationName": "Various cooking schools or restaurants in Ha Long Bay", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "indulge-in-vietnamese-cooking-classes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_indulge-in-vietnamese-cooking-classes.jpg" + }, + { + "name": "Relaxation and Wellness Retreats", + "description": "Escape the hustle and bustle of everyday life and embrace tranquility with a rejuvenating wellness retreat. Ha Long Bay offers a range of options, from luxurious spa resorts to serene yoga retreats on secluded islands. Pamper yourself with massages, meditation sessions, and holistic treatments while surrounded by the breathtaking natural beauty of the bay.", + "locationName": "Various spa resorts and retreat centers around Ha Long Bay", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "ha-long-bay", + "ref": "relaxation-and-wellness-retreats", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_relaxation-and-wellness-retreats.jpg" + }, + { + "name": "Explore the Enchanting Thien Cung Cave", + "description": "Venture into the depths of Thien Cung Cave (Heavenly Palace Cave) and be amazed by its stunning natural formations. Witness the intricate stalactites and stalagmites, illuminated by colorful lights, and learn about the legends and folklore surrounding this magical cave. It's a truly awe-inspiring experience that will leave you speechless.", + "locationName": "Thien Cung Cave", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ha-long-bay", + "ref": "explore-the-enchanting-thien-cung-cave", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ha-long-bay_explore-the-enchanting-thien-cung-cave.jpg" + }, + { + "name": "Explore the Pennsylvania State Capitol Building", + "description": "Immerse yourselves in the grandeur of the Pennsylvania State Capitol Building, a masterpiece of Beaux-Arts architecture. Take a guided tour to marvel at the opulent interiors, intricate artwork, and the stunning rotunda. Learn about the state's rich history and the workings of the government.", + "locationName": "Pennsylvania State Capitol Building", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "explore-the-pennsylvania-state-capitol-building", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_explore-the-pennsylvania-state-capitol-building.jpg" + }, + { + "name": "Enjoy Family Fun at City Island", + "description": "Escape to City Island, a recreational haven located on the Susquehanna River. Take a leisurely stroll along the riverfront, rent bikes to explore the island's trails, or have a picnic lunch in the park. Catch a baseball game at FNB Field, home to the Harrisburg Senators, or ride the City Island Railroad for a scenic journey. There's something for everyone to enjoy.", + "locationName": "City Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "enjoy-family-fun-at-city-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_enjoy-family-fun-at-city-island.jpg" + }, + { + "name": "Stroll Through the Historic Midtown Neighborhood", + "description": "Wander through the charming streets of Midtown Harrisburg, known for its Victorian-era architecture and vibrant atmosphere. Explore independent shops, art galleries, and trendy restaurants. Visit the Broad Street Market, a historic marketplace offering a diverse selection of local produce, artisanal goods, and international cuisine.", + "locationName": "Midtown Harrisburg", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "stroll-through-the-historic-midtown-neighborhood", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_stroll-through-the-historic-midtown-neighborhood.jpg" + }, + { + "name": "Cruise Along the Susquehanna River", + "description": "Embark on a scenic boat tour along the Susquehanna River and admire the city skyline from a different perspective. Enjoy the fresh air and picturesque views as you learn about Harrisburg's history and landmarks. Opt for a dinner cruise for a romantic evening or a sightseeing cruise to capture stunning photos.", + "locationName": "Susquehanna River", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "harrisburg", + "ref": "cruise-along-the-susquehanna-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_cruise-along-the-susquehanna-river.jpg" + }, + { + "name": "Hiking or Biking on the Capital Area Greenbelt", + "description": "Embark on a scenic adventure along the 20-mile Capital Area Greenbelt, a paved trail perfect for hiking, biking, or rollerblading. Enjoy the fresh air and picturesque views of the Susquehanna River, Wildwood Park, and various historic landmarks.", + "locationName": "Capital Area Greenbelt", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "harrisburg", + "ref": "hiking-or-biking-on-the-capital-area-greenbelt", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_hiking-or-biking-on-the-capital-area-greenbelt.jpg" + }, + { + "name": "Indulge in Farm-Fresh Flavors at the Broad Street Market", + "description": "Immerse yourself in the vibrant atmosphere of the Broad Street Market, one of the oldest continuously operating farmers' markets in the country. Sample local produce, artisanal cheeses, baked goods, and international cuisine from a diverse array of vendors.", + "locationName": "Broad Street Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "indulge-in-farm-fresh-flavors-at-the-broad-street-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_indulge-in-farm-fresh-flavors-at-the-broad-street-market.jpg" + }, + { + "name": "Catch a Show at the Whitaker Center for Science and the Arts", + "description": "Experience the magic of live performances, captivating exhibits, and educational programs at the Whitaker Center. From Broadway shows to planetarium presentations, there's something to ignite curiosity and inspire creativity for all ages.", + "locationName": "Whitaker Center for Science and the Arts", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "harrisburg", + "ref": "catch-a-show-at-the-whitaker-center-for-science-and-the-arts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_catch-a-show-at-the-whitaker-center-for-science-and-the-arts.jpg" + }, + { + "name": "Explore the Enchanting Hershey Gardens", + "description": "Take a short drive to Hershey and discover the beauty of Hershey Gardens. Wander through themed gardens, admire the vibrant blooms, and learn about the fascinating world of horticulture. Don't miss the Butterfly House for a magical encounter with fluttering friends.", + "locationName": "Hershey Gardens", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "explore-the-enchanting-hershey-gardens", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_explore-the-enchanting-hershey-gardens.jpg" + }, + { + "name": "Savor Craft Brews on the Harrisburg Beer Trail", + "description": "Embark on a self-guided tour of Harrisburg's thriving craft beer scene. Visit local breweries like Zeroday Brewing Co., Appalachian Brewing Company, and Troegs Independent Brewing, to sample unique flavors and discover your new favorite brew.", + "locationName": "Harrisburg Beer Trail", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "harrisburg", + "ref": "savor-craft-brews-on-the-harrisburg-beer-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_savor-craft-brews-on-the-harrisburg-beer-trail.jpg" + }, + { + "name": "Wildwood Park Nature Exploration", + "description": "Embark on a journey through diverse ecosystems at Wildwood Park. Hike or bike along scenic trails, observe native wildlife in their natural habitat, and learn about the park's conservation efforts. This immersive experience is perfect for nature enthusiasts and families seeking outdoor adventure.", + "locationName": "Wildwood Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "harrisburg", + "ref": "wildwood-park-nature-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_wildwood-park-nature-exploration.jpg" + }, + { + "name": "Antique Treasure Hunt in Midtown", + "description": "Discover hidden gems and unique finds in Harrisburg's Midtown district. Explore antique shops and vintage stores, where you can browse through a diverse collection of furniture, jewelry, clothing, and more. Uncover a piece of history and add a touch of character to your home.", + "locationName": "Midtown Harrisburg", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "antique-treasure-hunt-in-midtown", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_antique-treasure-hunt-in-midtown.jpg" + }, + { + "name": "Pennsylvania National Fire Museum", + "description": "Dive into the history of firefighting at the Pennsylvania National Fire Museum. Explore exhibits showcasing antique fire engines, equipment, and memorabilia. Learn about the brave men and women who have dedicated their lives to protecting our communities from fire.", + "locationName": "Pennsylvania National Fire Museum", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "pennsylvania-national-fire-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_pennsylvania-national-fire-museum.jpg" + }, + { + "name": "Sunset Riverboat Cruise", + "description": "Experience the beauty of Harrisburg from a different perspective with a relaxing sunset riverboat cruise on the Susquehanna River. Enjoy breathtaking views of the city skyline, the rolling hills, and the picturesque waterfront as you savor a delicious meal and live entertainment.", + "locationName": "Susquehanna River", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "harrisburg", + "ref": "sunset-riverboat-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_sunset-riverboat-cruise.jpg" + }, + { + "name": "Fort Hunter Mansion and Park", + "description": "Step back in time at Fort Hunter Mansion and Park, a historic landmark dating back to the 18th century. Explore the beautifully preserved mansion, stroll through the scenic park grounds, and learn about the region's rich history and culture.", + "locationName": "Fort Hunter Mansion and Park", + "duration": 2.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "fort-hunter-mansion-and-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_fort-hunter-mansion-and-park.jpg" + }, + { + "name": "Thrills at Hersheypark", + "description": "Embark on a day of excitement at Hersheypark, a renowned amusement park just a short drive from Harrisburg. Experience thrilling roller coasters, family-friendly rides, water attractions, and delectable chocolate treats. From the iconic wooden coaster, Comet, to the exhilarating Skyrush, there's something for every thrill-seeker. Don't miss the chance to explore ZooAmerica, a walk-through zoo within the park, and indulge in the sweetness of Hershey's Chocolate World.", + "locationName": "Hersheypark", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "harrisburg", + "ref": "thrills-at-hersheypark", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_thrills-at-hersheypark.jpg" + }, + { + "name": "Wine Tasting in the Susquehanna Heartland Wine Trail", + "description": "Embark on a delightful journey through the Susquehanna Heartland Wine Trail, where you can savor the flavors of local wineries. Explore charming vineyards, indulge in wine tastings, and learn about the region's viticulture. From crisp whites to bold reds, discover the perfect vintage to please your palate. Many wineries offer scenic vineyard views and cozy tasting rooms, making it an ideal experience for couples or groups of friends.", + "locationName": "Susquehanna Heartland Wine Trail", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "harrisburg", + "ref": "wine-tasting-in-the-susquehanna-heartland-wine-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_wine-tasting-in-the-susquehanna-heartland-wine-trail.jpg" + }, + { + "name": "Antique Shopping in Downtown Harrisburg", + "description": "Discover hidden treasures and unique finds while antique shopping in downtown Harrisburg. Explore charming antique shops and boutiques filled with vintage furniture, collectibles, jewelry, and more. Stroll through the historic streets and uncover one-of-a-kind pieces to add character to your home or wardrobe. Midtown Harrisburg is known for its antique stores, offering a diverse selection for every taste and budget.", + "locationName": "Downtown Harrisburg", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "antique-shopping-in-downtown-harrisburg", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_antique-shopping-in-downtown-harrisburg.jpg" + }, + { + "name": "Kayaking on the Susquehanna River", + "description": "Experience the beauty of the Susquehanna River up close with a kayaking adventure. Rent a kayak and paddle along the scenic waterways, enjoying the peaceful surroundings and observing local wildlife. Several rental companies offer guided tours and equipment rentals, catering to both beginners and experienced kayakers. Admire the city skyline from a different perspective and reconnect with nature on this memorable outdoor activity.", + "locationName": "Susquehanna River", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "kayaking-on-the-susquehanna-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_kayaking-on-the-susquehanna-river.jpg" + }, + { + "name": "Catch a Baseball Game at FNB Field", + "description": "Immerse yourself in the excitement of America's favorite pastime at FNB Field, home to the Harrisburg Senators, a Minor League Baseball team. Cheer on the home team, enjoy classic ballpark snacks, and experience the lively atmosphere of a baseball game. FNB Field offers a family-friendly environment with affordable ticket prices, making it a great option for a fun-filled evening out.", + "locationName": "FNB Field", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "harrisburg", + "ref": "catch-a-baseball-game-at-fnb-field", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/harrisburg_catch-a-baseball-game-at-fnb-field.jpg" + }, + { + "name": "Stroll Down the Malecón", + "description": "Take a leisurely walk along the iconic Malecón, Havana's famous seaside promenade. Enjoy stunning views of the ocean, admire the colorful vintage cars passing by, and soak up the vibrant atmosphere as locals chat, fish, and enjoy the sea breeze. This is a perfect activity for any time of day and a great way to experience the heart and soul of Havana.", + "locationName": "Malecón", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "havana", + "ref": "stroll-down-the-malec-n", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_stroll-down-the-malec-n.jpg" + }, + { + "name": "Explore Old Havana", + "description": "Step back in time and wander through the cobblestone streets of Old Havana, a UNESCO World Heritage Site. Marvel at the colonial architecture, visit historic plazas like Plaza Vieja and Plaza de la Catedral, and discover hidden gems around every corner. Be sure to visit the Catedral de San Cristóbal, a stunning example of Baroque architecture.", + "locationName": "Old Havana", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "havana", + "ref": "explore-old-havana", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_explore-old-havana.jpg" + }, + { + "name": "Immerse Yourself in Cuban Culture", + "description": "Experience the vibrant Cuban culture by visiting the Gran Teatro de La Habana, a stunning neo-baroque theater, or the Museo Nacional de Bellas Artes, showcasing Cuban art. In the evening, enjoy a live music performance at a local bar or catch a captivating dance show featuring traditional Cuban rhythms.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "havana", + "ref": "immerse-yourself-in-cuban-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_immerse-yourself-in-cuban-culture.jpg" + }, + { + "name": "Visit the Museum of the Revolution", + "description": "Delve into Cuba's rich history at the Museum of the Revolution, housed in the former Presidential Palace. Explore exhibits showcasing the country's struggle for independence, the revolution led by Fidel Castro, and the impact of communism on Cuban society. This is a must-visit for history buffs and those interested in understanding Cuba's complex past.", + "locationName": "Museum of the Revolution", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "havana", + "ref": "visit-the-museum-of-the-revolution", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_visit-the-museum-of-the-revolution.jpg" + }, + { + "name": "Take a Classic Car Tour", + "description": "Cruise through the streets of Havana in style with a classic car tour. Feel the wind in your hair as you ride in a vintage American convertible, taking in the sights and sounds of the city. Many tours offer customizable itineraries, allowing you to visit specific landmarks or explore different neighborhoods. This is a fun and unique way to experience Havana's charm.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "havana", + "ref": "take-a-classic-car-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_take-a-classic-car-tour.jpg" + }, + { + "name": "Dance the Night Away at a Salsa Club", + "description": "Experience the infectious rhythm of Cuban salsa at a local club. Whether you're a seasoned dancer or a beginner, you'll be swept up in the energy and passion of the music and the locals. Take a salsa lesson beforehand to learn some basic steps, or just jump in and let loose on the dance floor. It's a night of pure fun and cultural immersion.", + "locationName": "Casa de la Musica or 1830 Club", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "havana", + "ref": "dance-the-night-away-at-a-salsa-club", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_dance-the-night-away-at-a-salsa-club.jpg" + }, + { + "name": "Relax on the Beaches of Playas del Este", + "description": "Escape the city bustle and unwind on the pristine sands of Playas del Este, a string of beaches just a short drive from Havana. Soak up the sun, swim in the turquoise waters, or simply relax under the shade of a palm tree. Several beach bars and restaurants offer refreshments and local seafood, making it a perfect day trip for relaxation and rejuvenation.", + "locationName": "Playas del Este", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "havana", + "ref": "relax-on-the-beaches-of-playas-del-este", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_relax-on-the-beaches-of-playas-del-este.jpg" + }, + { + "name": "Explore the Lush Botanical Gardens", + "description": "Escape the city heat and immerse yourself in the tranquility of Havana's Botanical Gardens. Wander through diverse collections of tropical plants, orchids, and cacti. The Japanese Garden offers a serene atmosphere with its koi ponds and traditional architecture. It's a perfect place for nature lovers and those seeking a peaceful retreat.", + "locationName": "Jardin Botanico Nacional de Cuba", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "havana", + "ref": "explore-the-lush-botanical-gardens", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_explore-the-lush-botanical-gardens.jpg" + }, + { + "name": "Visit the Fortaleza de San Carlos de la Cabaña", + "description": "Step back in time at this 18th-century fortress, offering stunning views of Havana and the harbor. Explore the ramparts, tunnels, and museums within the fort, which showcase Cuba's military history. Don't miss the nightly cannon firing ceremony, a tradition dating back centuries.", + "locationName": "Fortaleza de San Carlos de la Cabaña", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "havana", + "ref": "visit-the-fortaleza-de-san-carlos-de-la-caba-a", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_visit-the-fortaleza-de-san-carlos-de-la-caba-a.jpg" + }, + { + "name": "Enjoy a Day Trip to Viñales Valley", + "description": "Embark on a scenic journey to Viñales Valley, a UNESCO World Heritage Site renowned for its dramatic limestone mountains, tobacco plantations, and traditional farming communities. Hike or horseback ride through the valley, visit a tobacco farm to learn about cigar making, and enjoy a farm-to-table lunch amidst the breathtaking scenery. It's a perfect escape into Cuba's rural beauty.", + "locationName": "Viñales Valley", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "havana", + "ref": "enjoy-a-day-trip-to-vi-ales-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_enjoy-a-day-trip-to-vi-ales-valley.jpg" + }, + { + "name": "Discover Hemingway's Haunts", + "description": "Embark on a literary pilgrimage to Ernest Hemingway's beloved Cuba. Visit Finca Vigía, his former home turned museum, and explore the rooms where he wrote some of his most famous works. Sip a daiquiri at El Floridita, his favorite bar, and try your luck at fishing in the waters he once sailed.", + "locationName": "Finca Vigía and El Floridita", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "havana", + "ref": "discover-hemingway-s-haunts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_discover-hemingway-s-haunts.jpg" + }, + { + "name": "Take a Cooking Class", + "description": "Delve into the heart of Cuban cuisine by joining a cooking class. Learn to prepare traditional dishes like ropa vieja, picadillo, and tostones, under the guidance of local chefs. Savor the fruits of your labor and gain a deeper appreciation for Cuban flavors.", + "locationName": "Various cooking schools and paladares", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "havana", + "ref": "take-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_take-a-cooking-class.jpg" + }, + { + "name": "Catch a Baseball Game", + "description": "Experience the passion of Cuban baseball at Estadio Latinoamericano. Cheer alongside enthusiastic fans as you witness the skill and athleticism of local teams. Immerse yourself in the vibrant atmosphere and understand why baseball is more than just a sport in Cuba.", + "locationName": "Estadio Latinoamericano", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "havana", + "ref": "catch-a-baseball-game", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_catch-a-baseball-game.jpg" + }, + { + "name": "Explore the Fábrica de Arte Cubano (FAC)", + "description": "Immerse yourself in Havana's contemporary art scene at the Fábrica de Arte Cubano (FAC). This unique art space housed in a former cooking oil factory showcases a diverse range of exhibitions, live music performances, film screenings, and more. Enjoy a drink at the bar and soak up the creative energy.", + "locationName": "Fábrica de Arte Cubano (FAC)", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "havana", + "ref": "explore-the-f-brica-de-arte-cubano-fac-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_explore-the-f-brica-de-arte-cubano-fac-.jpg" + }, + { + "name": "Ride in a Coco Taxi", + "description": "Zip around the city in a unique coco taxi, a three-wheeled scooter with a yellow, coconut-shaped shell. Enjoy the open air and get a different perspective of Havana's streets and sights. It's a fun and memorable way to experience local transportation.", + "locationName": "Throughout Havana", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "havana", + "ref": "ride-in-a-coco-taxi", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_ride-in-a-coco-taxi.jpg" + }, + { + "name": "Dive into the Underwater World of the Bay of Pigs", + "description": "Embark on an unforgettable scuba diving or snorkeling adventure in the Bay of Pigs, renowned for its pristine coral reefs and diverse marine life. Explore underwater caves, swim alongside colorful fish, and discover the hidden treasures of Cuba's Caribbean coast.", + "locationName": "Bay of Pigs", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "havana", + "ref": "dive-into-the-underwater-world-of-the-bay-of-pigs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_dive-into-the-underwater-world-of-the-bay-of-pigs.jpg" + }, + { + "name": "Hike to the Top of El Yunque", + "description": "Challenge yourself with a hike to the summit of El Yunque, Cuba's highest peak, located in the Sierra Maestra mountains. Enjoy breathtaking panoramic views of the surrounding landscapes, lush rainforests, and the distant coastline. This adventure is perfect for nature enthusiasts and those seeking stunning vistas.", + "locationName": "El Yunque", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "havana", + "ref": "hike-to-the-top-of-el-yunque", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_hike-to-the-top-of-el-yunque.jpg" + }, + { + "name": "Ride Horseback Through Viñales Valley", + "description": "Saddle up for a scenic horseback riding tour through the picturesque Viñales Valley. Admire the stunning mogotes (limestone hills), tobacco fields, and traditional farmhouses. This leisurely activity allows you to connect with nature and experience the authentic Cuban countryside.", + "locationName": "Viñales Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "havana", + "ref": "ride-horseback-through-vi-ales-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_ride-horseback-through-vi-ales-valley.jpg" + }, + { + "name": "Sail Away on a Catamaran Cruise", + "description": "Set sail on a relaxing catamaran cruise along the crystal-clear waters of the Caribbean Sea. Soak up the sun, enjoy refreshing sea breezes, and snorkel or swim in secluded coves. Many cruises offer delicious meals, open bars, and lively music, creating an unforgettable experience.", + "locationName": "Havana Harbor", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "havana", + "ref": "sail-away-on-a-catamaran-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_sail-away-on-a-catamaran-cruise.jpg" + }, + { + "name": "Experience Cuban Art and Music at the Callejón de Hamel", + "description": "Immerse yourself in the vibrant art and music scene of Havana at the Callejón de Hamel. This narrow alleyway is adorned with colorful murals, sculptures, and mosaics, showcasing the creativity of local artists. Enjoy live rumba performances and soak up the lively atmosphere of this unique cultural hub.", + "locationName": "Callejón de Hamel", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "havana", + "ref": "experience-cuban-art-and-music-at-the-callej-n-de-hamel", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/havana_experience-cuban-art-and-music-at-the-callej-n-de-hamel.jpg" + }, + { + "name": "Cu Chi Tunnels Tour", + "description": "Embark on a historical journey to the Cu Chi Tunnels, a vast network of underground tunnels used by the Viet Cong during the Vietnam War. Crawl through the narrow passages, learn about the tunnels' construction and significance, and gain insight into the resilience and ingenuity of the Vietnamese people.", + "locationName": "Cu Chi Tunnels", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "ho-chi-minh-city", + "ref": "cu-chi-tunnels-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_cu-chi-tunnels-tour.jpg" + }, + { + "name": "Ben Thanh Market Shopping Spree", + "description": "Immerse yourself in the vibrant atmosphere of Ben Thanh Market, a bustling hub of commerce and culture. Browse through a labyrinth of stalls selling everything from souvenirs and handicrafts to clothing, spices, and fresh produce. Practice your bargaining skills and find unique treasures to take home.", + "locationName": "Ben Thanh Market", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "ho-chi-minh-city", + "ref": "ben-thanh-market-shopping-spree", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_ben-thanh-market-shopping-spree.jpg" + }, + { + "name": "Saigon Street Food Adventure", + "description": "Embark on a culinary adventure through the streets of Ho Chi Minh City, sampling a variety of delicious street food dishes. From savory banh mi sandwiches and fragrant pho noodle soup to sweet chè desserts and refreshing sugarcane juice, tantalize your taste buds with the authentic flavors of Vietnam.", + "locationName": "Various street food stalls", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "ho-chi-minh-city", + "ref": "saigon-street-food-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_saigon-street-food-adventure.jpg" + }, + { + "name": "Mekong Delta Day Trip", + "description": "Escape the bustling city and venture into the scenic Mekong Delta. Cruise along the waterways, visit floating markets, explore local villages, and witness the traditional way of life in this unique region.", + "locationName": "Mekong Delta", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "ho-chi-minh-city", + "ref": "mekong-delta-day-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_mekong-delta-day-trip.jpg" + }, + { + "name": "Golden Dragon Water Puppet Show", + "description": "Experience the magic of Vietnamese water puppetry, a unique art form dating back centuries. Watch as skilled puppeteers bring colorful puppets to life on a water stage, accompanied by traditional music and captivating storytelling.", + "locationName": "Golden Dragon Water Puppet Theatre", + "duration": 1.5, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "ho-chi-minh-city", + "ref": "golden-dragon-water-puppet-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_golden-dragon-water-puppet-show.jpg" + }, + { + "name": "Notre Dame Cathedral Visit", + "description": "Step into a piece of French colonial history at the Notre Dame Cathedral. Admire the neo-Romanesque architecture, stained glass windows, and peaceful atmosphere. Capture stunning photos of this iconic landmark.", + "locationName": "Notre Dame Cathedral Basilica of Saigon", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "ho-chi-minh-city", + "ref": "notre-dame-cathedral-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_notre-dame-cathedral-visit.jpg" + }, + { + "name": "Rooftop Bar Hopping", + "description": "Savor panoramic views of the glittering cityscape while sipping on cocktails at some of Ho Chi Minh City's trendy rooftop bars. Enjoy the vibrant nightlife and soak up the energetic atmosphere.", + "locationName": "Various rooftop bars throughout the city", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "ho-chi-minh-city", + "ref": "rooftop-bar-hopping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_rooftop-bar-hopping.jpg" + }, + { + "name": "Bitexco Financial Tower Observation Deck", + "description": "Ascend to the Saigon Skydeck on the 49th floor of the Bitexco Financial Tower for breathtaking 360-degree views of the city. Spot iconic landmarks, observe the bustling streets below, and capture memorable photos.", + "locationName": "Bitexco Financial Tower", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ho-chi-minh-city", + "ref": "bitexco-financial-tower-observation-deck", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_bitexco-financial-tower-observation-deck.jpg" + }, + { + "name": "Cooking Class and Local Market Tour", + "description": "Embark on a culinary adventure with a local market tour and cooking class. Learn about Vietnamese ingredients, practice traditional cooking techniques, and create delicious dishes to enjoy.", + "locationName": "Various cooking schools and markets", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "ho-chi-minh-city", + "ref": "cooking-class-and-local-market-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_cooking-class-and-local-market-tour.jpg" + }, + { + "name": "Explore the Jade Emperor Pagoda", + "description": "Step into a world of vibrant colors and intricate details at the Jade Emperor Pagoda. This Taoist temple, built in the early 20th century, is a captivating masterpiece of architecture and religious art. Admire the ornate carvings, statues of deities, and serene gardens, and immerse yourself in the spiritual atmosphere.", + "locationName": "Jade Emperor Pagoda (Phuoc Hai Tu)", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ho-chi-minh-city", + "ref": "explore-the-jade-emperor-pagoda", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_explore-the-jade-emperor-pagoda.jpg" + }, + { + "name": "Cruise the Saigon River at Sunset", + "description": "Experience the magic of Ho Chi Minh City from a different perspective with a relaxing sunset cruise on the Saigon River. Watch the city lights twinkle as you glide along the water, enjoying the cool breeze and breathtaking views of the skyline. Some cruises offer dinner or live music for an extra special evening.", + "locationName": "Saigon River", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "ho-chi-minh-city", + "ref": "cruise-the-saigon-river-at-sunset", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_cruise-the-saigon-river-at-sunset.jpg" + }, + { + "name": "Catch a Show at the Saigon Opera House", + "description": "Indulge in a night of culture and elegance at the historic Saigon Opera House. This architectural gem hosts a variety of performances, from traditional Vietnamese opera and dance to international concerts and ballets. Dress up for a memorable evening and enjoy the opulent ambiance of this iconic venue.", + "locationName": "Saigon Opera House", + "duration": 2.5, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "ho-chi-minh-city", + "ref": "catch-a-show-at-the-saigon-opera-house", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_catch-a-show-at-the-saigon-opera-house.jpg" + }, + { + "name": "Unwind at a Rooftop Cafe", + "description": "Escape the bustling streets and find tranquility at one of Ho Chi Minh City's many rooftop cafes. Sip on a refreshing drink or savor a delicious meal while enjoying panoramic views of the cityscape. These rooftop havens offer a perfect blend of relaxation and stunning scenery.", + "locationName": "Various locations throughout the city", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ho-chi-minh-city", + "ref": "unwind-at-a-rooftop-cafe", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_unwind-at-a-rooftop-cafe.jpg" + }, + { + "name": "Discover History at the Reunification Palace", + "description": "Step back in time at the Reunification Palace, a historical landmark that witnessed the end of the Vietnam War. Explore the opulent rooms and halls, learn about the palace's role in the country's history, and gain insights into Vietnam's journey to reunification.", + "locationName": "Reunification Palace", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ho-chi-minh-city", + "ref": "discover-history-at-the-reunification-palace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_discover-history-at-the-reunification-palace.jpg" + }, + { + "name": "Tao Dan Park Morning Exercise", + "description": "Start your day like a local at Tao Dan Park, a green oasis in the heart of the city. Join the community in their morning routines, from tai chi and badminton to jogging and group exercises. Immerse yourself in the local culture and enjoy the fresh air before the city awakens.", + "locationName": "Tao Dan Park", + "duration": 1.5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "ho-chi-minh-city", + "ref": "tao-dan-park-morning-exercise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_tao-dan-park-morning-exercise.jpg" + }, + { + "name": "Giac Lam Pagoda Visit", + "description": "Escape the urban bustle and find serenity at Giac Lam Pagoda, the oldest Buddhist temple in Ho Chi Minh City. Explore the intricate architecture, tranquil gardens, and ornate statues. Witness the devotion of local worshippers and experience the peaceful atmosphere of this spiritual haven.", + "locationName": "Giac Lam Pagoda", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "ho-chi-minh-city", + "ref": "giac-lam-pagoda-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_giac-lam-pagoda-visit.jpg" + }, + { + "name": "Scooter Street Food Tour", + "description": "Embark on an exciting culinary adventure through the city on a scooter. Zip through the streets with a local guide, stopping at hidden gems and popular food stalls. Sample authentic Vietnamese dishes like banh mi, pho, and goi cuon, experiencing the true flavors of Ho Chi Minh City.", + "locationName": "Various locations throughout Ho Chi Minh City", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "ho-chi-minh-city", + "ref": "scooter-street-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_scooter-street-food-tour.jpg" + }, + { + "name": "Traditional Ao Dai Rental and Photoshoot", + "description": "Embrace Vietnamese culture by renting a traditional Ao Dai, the elegant national garment. Choose from a variety of colors and styles, then capture stunning photos at iconic landmarks or hidden corners of the city. This experience offers a unique souvenir and lasting memories.", + "locationName": "Ao Dai rental shops and various photo locations", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "ho-chi-minh-city", + "ref": "traditional-ao-dai-rental-and-photoshoot", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_traditional-ao-dai-rental-and-photoshoot.jpg" + }, + { + "name": "Live Music at Yoko Cafe", + "description": "Immerse yourself in Ho Chi Minh City's vibrant nightlife scene at Yoko Cafe. Enjoy live music performances by local bands, ranging from traditional Vietnamese tunes to contemporary genres. Sip on cocktails or local beers while soaking up the energetic atmosphere and connecting with fellow music enthusiasts.", + "locationName": "Yoko Cafe", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "ho-chi-minh-city", + "ref": "live-music-at-yoko-cafe", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/ho-chi-minh-city_live-music-at-yoko-cafe.jpg" + }, + { + "name": "Glacier Hiking and Ice Climbing", + "description": "Embark on a thrilling adventure across Iceland's stunning glaciers. Experienced guides will lead you on a hike across the icy terrain, teaching you basic ice climbing techniques. Marvel at the breathtaking views of crevasses, ice caves, and frozen landscapes. This activity is perfect for adventurous souls seeking a unique and unforgettable experience.", + "locationName": "Skaftafell National Park or Sólheimajökull Glacier", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "iceland", + "ref": "glacier-hiking-and-ice-climbing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_glacier-hiking-and-ice-climbing.jpg" + }, + { + "name": "Soak in the Blue Lagoon", + "description": "Indulge in a relaxing and rejuvenating experience at the world-renowned Blue Lagoon. Immerse yourself in the geothermal waters, rich in minerals, and let the warmth soothe your muscles. Enjoy the unique milky-blue color of the water and the surrounding volcanic landscape. This is a perfect activity for any time of day and a must-do for visitors to Iceland.", + "locationName": "Blue Lagoon", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "iceland", + "ref": "soak-in-the-blue-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_soak-in-the-blue-lagoon.jpg" + }, + { + "name": "Whale Watching Tour", + "description": "Set sail on a boat tour from Reykjavik, Akureyri, or Húsavík and witness the majestic whales in their natural habitat. Iceland's waters are home to various whale species, including humpback whales, minke whales, and orcas. Learn about these fascinating creatures from expert guides and enjoy the scenic beauty of the Icelandic coastline.", + "locationName": "Reykjavik, Akureyri, or Húsavík", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "iceland", + "ref": "whale-watching-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_whale-watching-tour.jpg" + }, + { + "name": "Explore the Golden Circle", + "description": "Embark on a scenic journey through Iceland's famous Golden Circle. Visit Thingvellir National Park, a UNESCO World Heritage site, and see the dramatic rift valley where the North American and Eurasian tectonic plates meet. Marvel at the Gullfoss waterfall, a powerful two-tiered cascade, and witness the Strokkur geyser erupting hot water high into the air.", + "locationName": "Thingvellir National Park, Gullfoss Waterfall, Strokkur Geyser", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "iceland", + "ref": "explore-the-golden-circle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_explore-the-golden-circle.jpg" + }, + { + "name": "Hunt for the Northern Lights", + "description": "Experience the magic of the Aurora Borealis, one of nature's most spectacular displays. During the winter months, venture out into the darkness and witness the dancing lights illuminating the night sky. This unforgettable experience is a must-do for any visitor to Iceland during the winter season.", + "locationName": "Various locations away from city lights", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "iceland", + "ref": "hunt-for-the-northern-lights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_hunt-for-the-northern-lights.jpg" + }, + { + "name": "Horseback Riding through Lava Fields", + "description": "Embark on a unique adventure exploring Iceland's rugged landscapes on horseback. Traverse ancient lava fields, volcanic craters, and scenic trails, experiencing the island's raw beauty from a different perspective. Connect with the friendly Icelandic horses, known for their strength and gentle temperament, as you ride through breathtaking scenery.", + "locationName": "Various locations throughout Iceland", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "iceland", + "ref": "horseback-riding-through-lava-fields", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_horseback-riding-through-lava-fields.jpg" + }, + { + "name": "Snorkeling in Silfra Fissure", + "description": "Dive into the crystal-clear waters of Silfra, a fissure between the North American and Eurasian tectonic plates. Snorkel through this underwater wonderland, marveling at the vibrant colors and incredible visibility. Experience the sensation of swimming between continents in one of the world's most unique diving and snorkeling locations.", + "locationName": "Thingvellir National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "iceland", + "ref": "snorkeling-in-silfra-fissure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_snorkeling-in-silfra-fissure.jpg" + }, + { + "name": "Caving Adventure in Vatnajökull Glacier", + "description": "Embark on a thrilling journey into the heart of Vatnajökull, Europe's largest glacier. Explore stunning ice caves adorned with mesmerizing blue ice formations, sculpted by the forces of nature. Witness the power and beauty of the glacial world as you venture deep into this icy wonderland.", + "locationName": "Vatnajökull National Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "iceland", + "ref": "caving-adventure-in-vatnaj-kull-glacier", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_caving-adventure-in-vatnaj-kull-glacier.jpg" + }, + { + "name": "Puffin Watching Boat Tour", + "description": "Set sail on a delightful boat tour to observe Iceland's adorable puffins in their natural habitat. Witness these charming seabirds with their colorful beaks nesting on coastal cliffs. Capture incredible photos and learn about the fascinating lives of these beloved creatures.", + "locationName": "Various locations along the coast", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "iceland", + "ref": "puffin-watching-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_puffin-watching-boat-tour.jpg" + }, + { + "name": "Reykjavík Food Tour", + "description": "Embark on a culinary journey through the vibrant city of Reykjavík, savoring the unique flavors of Icelandic cuisine. Sample traditional dishes like fresh seafood, lamb, and skyr, while exploring local restaurants and hidden gems. Discover the city's gastronomic scene and learn about Icelandic culinary traditions.", + "locationName": "Reykjavík", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "iceland", + "ref": "reykjav-k-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_reykjav-k-food-tour.jpg" + }, + { + "name": "Kayaking on Jokulsarlon Glacier Lagoon", + "description": "Embark on a serene kayaking adventure amidst the awe-inspiring Jokulsarlon Glacier Lagoon. Paddle through the tranquil waters, marveling at the floating icebergs, some as blue as sapphires, and witness the captivating beauty of this natural wonder.", + "locationName": "Jokulsarlon Glacier Lagoon", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "iceland", + "ref": "kayaking-on-jokulsarlon-glacier-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_kayaking-on-jokulsarlon-glacier-lagoon.jpg" + }, + { + "name": "Road Trip along the Ring Road", + "description": "Embark on an epic road trip adventure on Iceland's iconic Ring Road, Route 1. This scenic route circumnavigates the entire island, offering breathtaking landscapes, charming villages, and countless opportunities for exploration and discovery. From cascading waterfalls to dramatic coastlines, the Ring Road promises an unforgettable journey.", + "locationName": "Route 1 (Ring Road)", + "duration": 100, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "iceland", + "ref": "road-trip-along-the-ring-road", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_road-trip-along-the-ring-road.jpg" + }, + { + "name": "Visit the Westfjords", + "description": "Escape the crowds and venture into the remote and rugged beauty of the Westfjords. This region offers dramatic landscapes, towering cliffs, secluded beaches, and charming fishing villages. Hike to the Dynjandi waterfall, observe the diverse birdlife, and immerse yourself in the tranquility of this off-the-beaten-path destination.", + "locationName": "Westfjords", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "iceland", + "ref": "visit-the-westfjords", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_visit-the-westfjords.jpg" + }, + { + "name": "Relax in the Mývatn Nature Baths", + "description": "Indulge in a relaxing soak in the soothing geothermal waters of the Mývatn Nature Baths. Surrounded by volcanic landscapes, these milky-blue pools offer a tranquil escape. Unwind and rejuvenate as you soak in the mineral-rich waters and admire the stunning natural beauty of the area.", + "locationName": "Mývatn Nature Baths", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "iceland", + "ref": "relax-in-the-m-vatn-nature-baths", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_relax-in-the-m-vatn-nature-baths.jpg" + }, + { + "name": "Go Snowmobiling on a Glacier", + "description": "Experience the thrill of snowmobiling across the vast expanse of an Icelandic glacier. Feel the adrenaline rush as you zoom over the snow and ice, taking in the breathtaking panoramic views of the surrounding mountains and glaciers. This exhilarating adventure is perfect for thrill-seekers and winter sports enthusiasts.", + "locationName": "Various glaciers", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "iceland", + "ref": "go-snowmobiling-on-a-glacier", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_go-snowmobiling-on-a-glacier.jpg" + }, + { + "name": "Sea Kayaking Adventure", + "description": "Embark on a thrilling sea kayaking expedition along Iceland's rugged coastline. Paddle through crystal-clear waters, marvel at towering cliffs, and explore hidden coves and sea caves. Keep an eye out for playful seals and diverse birdlife as you experience the beauty of Iceland from a unique perspective.", + "locationName": "Various coastal locations", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "iceland", + "ref": "sea-kayaking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_sea-kayaking-adventure.jpg" + }, + { + "name": "Helicopter Tour over Volcanoes", + "description": "Take to the skies for an unforgettable helicopter tour that showcases Iceland's dramatic volcanic landscapes. Soar over active volcanoes, witness geothermal wonders like bubbling mud pools and steaming fumaroles, and enjoy breathtaking aerial views of glaciers, lava fields, and rugged mountains.", + "locationName": "Various volcanic regions", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "iceland", + "ref": "helicopter-tour-over-volcanoes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_helicopter-tour-over-volcanoes.jpg" + }, + { + "name": "Black Sand Beach Horse Riding", + "description": "Experience the unique Icelandic landscape on horseback with a memorable ride along the black sand beaches of the south coast. Feel the wind in your hair as you trot alongside the powerful Atlantic waves, admiring the dramatic cliffs and rock formations. This is a perfect activity for nature lovers and adventure seekers.", + "locationName": "Vík í Mýrdal or other black sand beaches", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "iceland", + "ref": "black-sand-beach-horse-riding", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_black-sand-beach-horse-riding.jpg" + }, + { + "name": "Reykjavík Nightlife Experience", + "description": "Discover the vibrant nightlife scene of Reykjavík, known for its friendly atmosphere and diverse music offerings. Explore the city's bars and clubs, enjoy live music performances, and experience the unique Icelandic culture of late-night revelry.", + "locationName": "Reykjavík city center", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "iceland", + "ref": "reykjav-k-nightlife-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_reykjav-k-nightlife-experience.jpg" + }, + { + "name": "Local Farm Visit and Icelandic Food Tasting", + "description": "Immerse yourself in Icelandic culture with a visit to a local farm. Learn about traditional farming practices, interact with friendly farm animals, and enjoy a tasting of authentic Icelandic food products, such as fresh dairy, smoked lamb, and homemade bread.", + "locationName": "Various rural locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "iceland", + "ref": "local-farm-visit-and-icelandic-food-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/iceland_local-farm-visit-and-icelandic-food-tasting.jpg" + }, + { + "name": "Hike the Kamikochi Valley", + "description": "Embark on a breathtaking journey through the Kamikochi Valley, a natural wonderland renowned for its pristine beauty. Stroll along well-maintained trails, surrounded by towering mountains, crystal-clear rivers, and lush forests. Witness the iconic Kappa Bridge, a picturesque symbol of the valley, and be captivated by the serenity of Taisho Pond, reflecting the majestic peaks.", + "locationName": "Kamikochi Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "japan-alps", + "ref": "hike-the-kamikochi-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_hike-the-kamikochi-valley.jpg" + }, + { + "name": "Skiing in Hakuba", + "description": "Experience the thrill of skiing or snowboarding in Hakuba, a world-class winter sports destination. With its abundant snowfall and diverse terrain, Hakuba offers something for all skill levels. Carve down powdery slopes, enjoy stunning mountain vistas, and après-ski in charming villages.", + "locationName": "Hakuba Valley", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "japan-alps", + "ref": "skiing-in-hakuba", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_skiing-in-hakuba.jpg" + }, + { + "name": "Soak in an Onsen", + "description": "Indulge in the ultimate relaxation experience by immersing yourself in a traditional Japanese onsen. Surrounded by serene landscapes, these natural hot springs offer therapeutic benefits and a sense of tranquility. Let the warm mineral-rich waters soothe your body and mind, rejuvenating you after a day of adventure.", + "locationName": "Various locations throughout the Japanese Alps", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "japan-alps", + "ref": "soak-in-an-onsen", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_soak-in-an-onsen.jpg" + }, + { + "name": "Visit Matsumoto Castle", + "description": "Step back in time and explore Matsumoto Castle, a historic landmark known as the \"Crow Castle\" for its black exterior. Admire its unique architecture, including its impressive main keep and intricate network of walls and moats. Delve into the castle's rich history and enjoy panoramic views of the surrounding city.", + "locationName": "Matsumoto City", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "japan-alps", + "ref": "visit-matsumoto-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_visit-matsumoto-castle.jpg" + }, + { + "name": "Experience Local Culture", + "description": "Immerse yourself in the unique culture of the mountain communities. Visit traditional villages, such as Shirakawa-go and Gokayama, with their distinctive gassho-zukuri farmhouses. Sample local cuisine, including soba noodles and mountain vegetables, and participate in cultural events or workshops.", + "locationName": "Various locations throughout the Japanese Alps", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "japan-alps", + "ref": "experience-local-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_experience-local-culture.jpg" + }, + { + "name": "Whitewater Rafting on the Yoshino River", + "description": "Experience the thrill of whitewater rafting on the Yoshino River, surrounded by the stunning scenery of the Japanese Alps. Navigate through rapids with experienced guides, enjoying the adrenaline rush and breathtaking views. This activity is perfect for adventure seekers and nature lovers.", + "locationName": "Yoshino River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "japan-alps", + "ref": "whitewater-rafting-on-the-yoshino-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_whitewater-rafting-on-the-yoshino-river.jpg" + }, + { + "name": "Cycling the Shimanami Kaido", + "description": "Embark on a scenic cycling journey along the Shimanami Kaido, a 70-kilometer route connecting Japan's main island of Honshu to Shikoku. Enjoy breathtaking views of the Seto Inland Sea and the surrounding islands as you pedal across bridges and through charming coastal towns. This activity is suitable for all fitness levels and offers a unique way to explore the region.", + "locationName": "Shimanami Kaido", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "japan-alps", + "ref": "cycling-the-shimanami-kaido", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_cycling-the-shimanami-kaido.jpg" + }, + { + "name": "Stargazing in the Mountains", + "description": "Escape the city lights and immerse yourself in the beauty of the night sky in the Japanese Alps. With minimal light pollution, the mountains offer exceptional stargazing opportunities. Join a guided tour or find a secluded spot to marvel at the constellations and the Milky Way.", + "locationName": "Various locations in the Japanese Alps", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "japan-alps", + "ref": "stargazing-in-the-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_stargazing-in-the-mountains.jpg" + }, + { + "name": "Exploring the Nakasendo Trail", + "description": "Step back in time and walk along the historic Nakasendo Trail, an ancient route that once connected Kyoto and Edo (Tokyo). Hike through charming villages, past traditional houses, and through serene forests, experiencing the rich history and culture of the region.", + "locationName": "Nakasendo Trail", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "japan-alps", + "ref": "exploring-the-nakasendo-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_exploring-the-nakasendo-trail.jpg" + }, + { + "name": "Visiting Local Craft Breweries", + "description": "Discover the growing craft beer scene in the Japanese Alps. Visit local breweries, sample unique and flavorful beers, and learn about the brewing process. Many breweries offer tours and tastings, providing a fun and educational experience for beer enthusiasts.", + "locationName": "Various locations in the Japanese Alps", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "japan-alps", + "ref": "visiting-local-craft-breweries", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_visiting-local-craft-breweries.jpg" + }, + { + "name": "Snowshoeing in the Winter Wonderland", + "description": "Strap on snowshoes and embark on a magical journey through the snow-covered forests and meadows of the Japanese Alps. Discover hidden trails, frozen waterfalls, and breathtaking panoramas, all while enjoying the peaceful serenity of the winter landscape. This activity is perfect for nature lovers and those seeking a unique winter experience.", + "locationName": "Various locations throughout the Japanese Alps", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "japan-alps", + "ref": "snowshoeing-in-the-winter-wonderland", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_snowshoeing-in-the-winter-wonderland.jpg" + }, + { + "name": "Indulge in a Traditional Tea Ceremony", + "description": "Immerse yourself in Japanese culture with an authentic tea ceremony experience. Learn about the intricate rituals and symbolism behind this ancient tradition while enjoying the delicate flavors of matcha tea and traditional sweets. This serene and enriching activity offers a glimpse into the heart of Japanese hospitality and aesthetics.", + "locationName": "Local tea houses or cultural centers", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "japan-alps", + "ref": "indulge-in-a-traditional-tea-ceremony", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_indulge-in-a-traditional-tea-ceremony.jpg" + }, + { + "name": "Go Birdwatching in Diverse Habitats", + "description": "The Japanese Alps offer a haven for birdwatchers with its diverse range of habitats, from alpine meadows to dense forests. Spot vibrant kingfishers, majestic eagles, and a variety of other bird species while enjoying the tranquility of nature. This activity is perfect for those seeking a peaceful and rewarding experience in the mountains.", + "locationName": "Kamikochi National Park, Norikura Kogen Highlands", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "japan-alps", + "ref": "go-birdwatching-in-diverse-habitats", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_go-birdwatching-in-diverse-habitats.jpg" + }, + { + "name": "Capture Stunning Landscapes on a Photography Tour", + "description": "Embark on a photography tour led by a local expert and capture the breathtaking beauty of the Japanese Alps. Learn about composition, lighting, and techniques while exploring iconic viewpoints, hidden waterfalls, and charming villages. This activity is ideal for photography enthusiasts of all levels who want to create lasting memories.", + "locationName": "Various locations with scenic viewpoints", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "japan-alps", + "ref": "capture-stunning-landscapes-on-a-photography-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_capture-stunning-landscapes-on-a-photography-tour.jpg" + }, + { + "name": "Savor Local Delights at a Mountain Restaurant", + "description": "Treat your taste buds to the unique flavors of the Japanese Alps at a charming mountain restaurant. Sample regional specialties such as soba noodles, mountain vegetables, and fresh river fish while enjoying panoramic views of the surrounding peaks. This culinary experience offers a delicious way to connect with the local culture and cuisine.", + "locationName": "Restaurants in mountain villages or ski resorts", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "japan-alps", + "ref": "savor-local-delights-at-a-mountain-restaurant", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_savor-local-delights-at-a-mountain-restaurant.jpg" + }, + { + "name": "Climbing Mount Fuji", + "description": "Embark on an unforgettable journey to the summit of Mount Fuji, Japan's iconic and highest peak. Witness breathtaking panoramic views as you ascend through various trails, each offering unique challenges and rewards. Reach the top and be greeted by a spectacular sunrise or sunset, an experience that will stay with you forever.", + "locationName": "Mount Fuji", + "duration": 12, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "japan-alps", + "ref": "climbing-mount-fuji", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_climbing-mount-fuji.jpg" + }, + { + "name": "Kayaking on Lake Aoki", + "description": "Experience the tranquility of Lake Aoki, nestled amidst the stunning landscapes of the Japanese Alps. Rent a kayak and paddle through the crystal-clear waters, surrounded by lush forests and towering mountains. Enjoy a peaceful escape in nature and capture the beauty of the lake from a unique perspective.", + "locationName": "Lake Aoki", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "japan-alps", + "ref": "kayaking-on-lake-aoki", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_kayaking-on-lake-aoki.jpg" + }, + { + "name": "Exploring the Jigokudani Monkey Park", + "description": "Discover the fascinating world of Japanese macaques, also known as snow monkeys, at the Jigokudani Monkey Park. Observe these intelligent primates in their natural habitat as they bathe in the onsen hot springs and interact with each other. Capture adorable photos and learn about their unique behaviors and adaptations to the mountain environment.", + "locationName": "Jigokudani Monkey Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "japan-alps", + "ref": "exploring-the-jigokudani-monkey-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_exploring-the-jigokudani-monkey-park.jpg" + }, + { + "name": "Paragliding over the Mountains", + "description": "Soar through the skies and experience the thrill of paragliding over the majestic Japanese Alps. Enjoy breathtaking aerial views of the mountains, valleys, and forests below. Feel the rush of adrenaline as you glide through the air, accompanied by experienced instructors who ensure your safety and provide guidance throughout the flight.", + "locationName": "Various locations throughout the Japanese Alps", + "duration": 1, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "japan-alps", + "ref": "paragliding-over-the-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_paragliding-over-the-mountains.jpg" + }, + { + "name": "Visiting Shirakawa-go", + "description": "Step back in time and explore the charming village of Shirakawa-go, a UNESCO World Heritage Site known for its traditional gassho-zukuri houses with their distinctive steep thatched roofs. Wander through the village's narrow streets, learn about its unique history and culture, and admire the picturesque landscapes that surround it.", + "locationName": "Shirakawa-go", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "japan-alps", + "ref": "visiting-shirakawa-go", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/japan-alps_visiting-shirakawa-go.jpg" + }, + { + "name": "Hike Mount Hallasan", + "description": "Embark on an unforgettable journey to the summit of Mount Hallasan, the highest peak in South Korea. Witness breathtaking panoramic views of the island, volcanic craters, and lush forests as you ascend through diverse trails. This challenging hike offers a rewarding experience for adventure seekers and nature enthusiasts.", + "locationName": "Hallasan National Park", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "jeju-island", + "ref": "hike-mount-hallasan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_hike-mount-hallasan.jpg" + }, + { + "name": "Explore the Manjanggul Lava Tube", + "description": "Delve into the mysterious depths of the Manjanggul Lava Tube, one of the longest lava tubes in the world. Marvel at the unique geological formations, including lava stalactites and stalagmites, and learn about the volcanic history of Jeju Island. This subterranean adventure offers a fascinating glimpse into the island's hidden wonders.", + "locationName": "Manjanggul Lava Tube", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jeju-island", + "ref": "explore-the-manjanggul-lava-tube", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_explore-the-manjanggul-lava-tube.jpg" + }, + { + "name": "Relax on Hyeopjae Beach", + "description": "Escape to the pristine shores of Hyeopjae Beach, renowned for its white sand, crystal-clear turquoise waters, and stunning coastal views. Bask in the sun, swim in the refreshing ocean, or simply stroll along the beach and enjoy the tranquil atmosphere. This idyllic beach destination is perfect for relaxation and rejuvenation.", + "locationName": "Hyeopjae Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "jeju-island", + "ref": "relax-on-hyeopjae-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_relax-on-hyeopjae-beach.jpg" + }, + { + "name": "Visit the Jeju Folk Village Museum", + "description": "Step back in time at the Jeju Folk Village Museum, a living history museum showcasing traditional Korean life. Explore authentic thatched-roof houses, observe skilled artisans practicing traditional crafts, and learn about the unique culture and customs of Jeju Island. This immersive experience offers a fascinating glimpse into the island's rich heritage.", + "locationName": "Jeju Folk Village Museum", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jeju-island", + "ref": "visit-the-jeju-folk-village-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_visit-the-jeju-folk-village-museum.jpg" + }, + { + "name": "Indulge in Local Cuisine", + "description": "Embark on a culinary journey through Jeju Island's diverse gastronomic scene. Sample fresh seafood delicacies, savor traditional Korean dishes with a local twist, and discover unique island specialties. From cozy family-run restaurants to upscale dining establishments, Jeju Island offers a delightful array of flavors to tantalize your taste buds.", + "locationName": "Various restaurants and markets", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jeju-island", + "ref": "indulge-in-local-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_indulge-in-local-cuisine.jpg" + }, + { + "name": "Horseback Riding on Jeju's Coast", + "description": "Embark on a scenic horseback riding adventure along the picturesque coastline of Jeju Island. Several ranches offer guided tours suitable for all skill levels, allowing you to trot through verdant fields, sandy beaches, and volcanic landscapes while enjoying the fresh ocean breeze and breathtaking views.", + "locationName": "Various ranches along the coast", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "jeju-island", + "ref": "horseback-riding-on-jeju-s-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_horseback-riding-on-jeju-s-coast.jpg" + }, + { + "name": "Delve into the O'sulloc Tea Museum and Green Tea Fields", + "description": "Immerse yourself in the world of Korean tea culture at the O'sulloc Tea Museum. Learn about the history and process of tea production, explore the lush green tea fields, and indulge in a delightful tea tasting experience. Don't miss the opportunity to savor unique tea-infused treats and shop for exquisite tea products.", + "locationName": "O'sulloc Tea Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jeju-island", + "ref": "delve-into-the-o-sulloc-tea-museum-and-green-tea-fields", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_delve-into-the-o-sulloc-tea-museum-and-green-tea-fields.jpg" + }, + { + "name": "Go on a Submarine Tour", + "description": "Embark on an unforgettable underwater adventure with a submarine tour. Descend into the depths of the ocean and witness the vibrant marine life of Jeju Island. Marvel at colorful fish, coral reefs, and other fascinating sea creatures as you explore the underwater realm.", + "locationName": "Seogwipo Submarine", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "jeju-island", + "ref": "go-on-a-submarine-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_go-on-a-submarine-tour.jpg" + }, + { + "name": "Chase Waterfalls: Jeongbang and Cheonjiyeon", + "description": "Discover the mesmerizing beauty of Jeju's waterfalls. Visit Jeongbang Waterfall, cascading directly into the ocean, and Cheonjiyeon Waterfall, surrounded by lush greenery and folklore. Hike the scenic trails, capture stunning photos, and enjoy the refreshing mist and tranquility of these natural wonders.", + "locationName": "Jeongbang and Cheonjiyeon Waterfalls", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "jeju-island", + "ref": "chase-waterfalls-jeongbang-and-cheonjiyeon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_chase-waterfalls-jeongbang-and-cheonjiyeon.jpg" + }, + { + "name": "Kayaking or Stand-Up Paddleboarding", + "description": "Explore the crystal-clear waters of Jeju Island at your own pace with kayaking or stand-up paddleboarding. Rent equipment and venture along the coastline, discovering hidden coves, secluded beaches, and breathtaking views. This activity is perfect for enjoying the serenity of the ocean and getting some exercise.", + "locationName": "Various beaches and coastal areas", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jeju-island", + "ref": "kayaking-or-stand-up-paddleboarding", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_kayaking-or-stand-up-paddleboarding.jpg" + }, + { + "name": "Stargazing at Seongsan Ilchulbong Peak", + "description": "Escape the city lights and venture to Seongsan Ilchulbong Peak, also known as Sunrise Peak, for a breathtaking stargazing experience. This volcanic cone offers minimal light pollution, making it an ideal spot to marvel at the constellations and Milky Way. Pack a blanket, some snacks, and enjoy a peaceful night under the stars.", + "locationName": "Seongsan Ilchulbong Peak", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "jeju-island", + "ref": "stargazing-at-seongsan-ilchulbong-peak", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_stargazing-at-seongsan-ilchulbong-peak.jpg" + }, + { + "name": "Immerse Yourself in Art at Jeju Museum of Art", + "description": "Delve into the vibrant art scene of Jeju Island at the Jeju Museum of Art. Admire a diverse collection of contemporary and traditional works by Korean and international artists. The museum's architecture is also a sight to behold, blending seamlessly with the surrounding natural landscape.", + "locationName": "Jeju Museum of Art", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jeju-island", + "ref": "immerse-yourself-in-art-at-jeju-museum-of-art", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_immerse-yourself-in-art-at-jeju-museum-of-art.jpg" + }, + { + "name": "Visit the Spirited Garden", + "description": "Embark on a tranquil journey through the Spirited Garden, a meticulously designed bonsai garden showcasing the harmony between nature and art. Stroll along winding paths, admire miniature landscapes, and find inner peace amidst the serene atmosphere. The garden also features a traditional Korean tea house where you can enjoy a cup of locally grown tea.", + "locationName": "Spirited Garden", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "jeju-island", + "ref": "visit-the-spirited-garden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_visit-the-spirited-garden.jpg" + }, + { + "name": "Discover the Mysterious Yongmeori Coast", + "description": "Embark on a scenic coastal walk along the Yongmeori Coast, known for its unique rock formations and dramatic cliffs carved by volcanic activity. Explore hidden caves, witness the power of the waves crashing against the shore, and capture stunning photographs of this geological wonder.", + "locationName": "Yongmeori Coast", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "jeju-island", + "ref": "discover-the-mysterious-yongmeori-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_discover-the-mysterious-yongmeori-coast.jpg" + }, + { + "name": "Shop for Local Treasures at Jeju Dongmun Market", + "description": "Immerse yourself in the bustling atmosphere of Jeju Dongmun Market, a traditional Korean market overflowing with local goods and culinary delights. Browse through stalls selling fresh produce, seafood, handicrafts, souvenirs, and clothing. Don't forget to sample some of the island's famous street food!", + "locationName": "Jeju Dongmun Market", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jeju-island", + "ref": "shop-for-local-treasures-at-jeju-dongmun-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_shop-for-local-treasures-at-jeju-dongmun-market.jpg" + }, + { + "name": "Island Hopping Adventure", + "description": "Embark on a boat tour to explore the cluster of smaller islands surrounding Jeju. Discover hidden coves, snorkel in crystal-clear waters, and soak up the sun on secluded beaches. Some tours even offer fishing opportunities or visits to unique island villages.", + "locationName": "Jeju's surrounding islands (e.g., Udo, Gapado, Biyangdo)", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "jeju-island", + "ref": "island-hopping-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_island-hopping-adventure.jpg" + }, + { + "name": "Cycling the Scenic Coastal Roads", + "description": "Rent a bicycle and embark on a leisurely ride along Jeju's picturesque coastal roads. Enjoy breathtaking ocean views, stop at charming cafes, and discover hidden beaches along the way. Several designated cycling paths offer safe and enjoyable routes for all skill levels.", + "locationName": "Jeju's coastal roads", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jeju-island", + "ref": "cycling-the-scenic-coastal-roads", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_cycling-the-scenic-coastal-roads.jpg" + }, + { + "name": "Soak in the Healing Waters of a Traditional Spa", + "description": "Indulge in a rejuvenating experience at one of Jeju's traditional Korean spas, known as jjimjilbang. Relax in various saunas and hot tubs, enjoy a body scrub, and experience unique treatments like jade stone therapy. Many spas also offer swimming pools, fitness centers, and dining options.", + "locationName": "Various jjimjilbangs throughout Jeju", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "jeju-island", + "ref": "soak-in-the-healing-waters-of-a-traditional-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_soak-in-the-healing-waters-of-a-traditional-spa.jpg" + }, + { + "name": "Tee Off at a World-Class Golf Course", + "description": "Jeju boasts several world-renowned golf courses with stunning landscapes and challenging layouts. Whether you're a seasoned golfer or a beginner, enjoy a round of golf amidst volcanic scenery and ocean views.", + "locationName": "Various golf courses across Jeju", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "jeju-island", + "ref": "tee-off-at-a-world-class-golf-course", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_tee-off-at-a-world-class-golf-course.jpg" + }, + { + "name": "Discover the Wonders of Jeju's Underwater World", + "description": "Explore the vibrant marine life and unique underwater landscapes of Jeju through scuba diving or snorkeling. Numerous dive sites offer opportunities to encounter colorful fish, coral reefs, and even shipwrecks.", + "locationName": "Various dive sites around Jeju", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "jeju-island", + "ref": "discover-the-wonders-of-jeju-s-underwater-world", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jeju-island_discover-the-wonders-of-jeju-s-underwater-world.jpg" + }, + { + "name": "Explore Petra, the Rose City", + "description": "Step back in time and discover the ancient Nabataean city of Petra, carved into the rose-colored sandstone cliffs. Explore the iconic Treasury, Monastery, and Royal Tombs, and marvel at the architectural genius of this UNESCO World Heritage site.", + "locationName": "Petra", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "jordan", + "ref": "explore-petra-the-rose-city", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_explore-petra-the-rose-city.jpg" + }, + { + "name": "Camp Under the Stars in Wadi Rum", + "description": "Embark on a desert adventure in the vast Wadi Rum, known for its dramatic landscapes of sandstone mountains and red sand dunes. Spend a night camping under the starry sky, enjoy traditional Bedouin hospitality, and experience the magic of the desert.", + "locationName": "Wadi Rum", + "duration": 24, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "jordan", + "ref": "camp-under-the-stars-in-wadi-rum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_camp-under-the-stars-in-wadi-rum.jpg" + }, + { + "name": "Discover the Roman Ruins of Jerash", + "description": "Explore the well-preserved Roman city of Jerash, known as the \"Pompeii of the East.\" Walk through the colonnaded streets, admire the impressive Hadrian's Arch, and visit the Oval Plaza and the South Theatre. Jerash offers a fascinating glimpse into Roman history and architecture.", + "locationName": "Jerash", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "jordan", + "ref": "discover-the-roman-ruins-of-jerash", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_discover-the-roman-ruins-of-jerash.jpg" + }, + { + "name": "Snorkel or Scuba Dive in the Red Sea", + "description": "Discover the underwater world of the Red Sea at Aqaba. Go snorkeling or scuba diving to explore vibrant coral reefs, encounter colorful fish, and experience the beauty of marine life.", + "locationName": "Aqaba", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "jordan", + "ref": "snorkel-or-scuba-dive-in-the-red-sea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_snorkel-or-scuba-dive-in-the-red-sea.jpg" + }, + { + "name": "Hike the Dana Biosphere Reserve", + "description": "Embark on a scenic hike through the Dana Biosphere Reserve, a stunning natural landscape with diverse ecosystems ranging from sandstone cliffs to lush valleys. Encounter rare wildlife, explore hidden canyons, and enjoy breathtaking views of the surrounding mountains.", + "locationName": "Dana Biosphere Reserve", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "jordan", + "ref": "hike-the-dana-biosphere-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_hike-the-dana-biosphere-reserve.jpg" + }, + { + "name": "Indulge in a Traditional Cooking Class", + "description": "Immerse yourself in Jordanian culture with a hands-on cooking class. Learn the secrets of preparing local dishes like mansaf (lamb cooked in yogurt sauce) and maqluba (upside-down rice and vegetable casserole) under the guidance of experienced chefs. Enjoy your delicious creations afterward.", + "locationName": "Amman or other major cities", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "jordan", + "ref": "indulge-in-a-traditional-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_indulge-in-a-traditional-cooking-class.jpg" + }, + { + "name": "Explore the Vibrant Souqs of Amman", + "description": "Get lost in the bustling atmosphere of Amman's traditional souqs. Wander through narrow alleyways filled with colorful stalls selling spices, textiles, handicrafts, and souvenirs. Practice your bargaining skills and discover unique treasures to take home.", + "locationName": "Amman", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "jordan", + "ref": "explore-the-vibrant-souqs-of-amman", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_explore-the-vibrant-souqs-of-amman.jpg" + }, + { + "name": "Relax in a Hammam", + "description": "Experience the ultimate relaxation with a traditional hammam experience. Enjoy a steam bath, body scrub, and massage, leaving you feeling refreshed and rejuvenated. This is a perfect way to unwind after a day of exploring Jordan's historical sites.", + "locationName": "Amman or other major cities", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "jordan", + "ref": "relax-in-a-hammam", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_relax-in-a-hammam.jpg" + }, + { + "name": "Visit the Umm Qais Archaeological Site", + "description": "Step back in time at the Umm Qais archaeological site, boasting impressive Roman ruins with panoramic views of the Sea of Galilee and the Golan Heights. Explore the well-preserved theater, basilica, and other structures, and imagine life in this ancient city.", + "locationName": "Umm Qais", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jordan", + "ref": "visit-the-umm-qais-archaeological-site", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_visit-the-umm-qais-archaeological-site.jpg" + }, + { + "name": "Horseback Riding in Wadi Rum", + "description": "Embark on a unique desert adventure by exploring the vast landscapes of Wadi Rum on horseback. Traverse the red sands, canyons, and rock formations, following ancient Bedouin trails and feeling a sense of connection with the nomadic culture. Whether you're a seasoned rider or a beginner, experienced guides will lead you through breathtaking scenery, offering an unforgettable perspective of Wadi Rum's beauty.", + "locationName": "Wadi Rum", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "jordan", + "ref": "horseback-riding-in-wadi-rum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_horseback-riding-in-wadi-rum.jpg" + }, + { + "name": "Birdwatching at Azraq Wetland Reserve", + "description": "Escape the desert landscapes and discover a haven for birdlife at the Azraq Wetland Reserve. This oasis, fed by a natural spring, attracts a diverse array of migratory and resident birds. Embark on a guided birdwatching tour to spot species like herons, eagles, and songbirds, while learning about the reserve's conservation efforts and unique ecosystem.", + "locationName": "Azraq Wetland Reserve", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "jordan", + "ref": "birdwatching-at-azraq-wetland-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_birdwatching-at-azraq-wetland-reserve.jpg" + }, + { + "name": "Hike to the Monastery at Petra by Night", + "description": "Experience the magic of Petra under the starry sky with a night hike to the Monastery. As the path is illuminated by hundreds of candles, follow the ancient trail and marvel at the architectural wonder of the Monastery bathed in soft light. The serene atmosphere and the unique perspective create an unforgettable and romantic experience.", + "locationName": "Petra", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "jordan", + "ref": "hike-to-the-monastery-at-petra-by-night", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_hike-to-the-monastery-at-petra-by-night.jpg" + }, + { + "name": "Enjoy a Traditional Jordanian Meal with a Local Family", + "description": "Immerse yourself in Jordanian culture and hospitality by sharing a traditional meal with a local family. Experience the warmth of Jordanian homes, learn about their customs and traditions, and savor authentic home-cooked dishes like mansaf, maqluba, and knafeh. This cultural exchange offers a unique opportunity to connect with locals and create lasting memories.", + "locationName": "Various locations (Amman, Madaba, etc.)", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "jordan", + "ref": "enjoy-a-traditional-jordanian-meal-with-a-local-family", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_enjoy-a-traditional-jordanian-meal-with-a-local-family.jpg" + }, + { + "name": "Shop for Handicrafts at the Jordan River Foundation Showroom", + "description": "Discover the beauty of Jordanian craftsmanship at the Jordan River Foundation Showroom. This non-profit organization supports local artisans, particularly women, by showcasing their traditional handicrafts. Browse through a collection of hand-woven rugs, pottery, embroidery, and other unique items, knowing that your purchase empowers local communities and preserves cultural heritage.", + "locationName": "Amman", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "jordan", + "ref": "shop-for-handicrafts-at-the-jordan-river-foundation-showroom", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_shop-for-handicrafts-at-the-jordan-river-foundation-showroom.jpg" + }, + { + "name": "Hot Air Balloon Ride Over Wadi Rum", + "description": "Experience the breathtaking beauty of Wadi Rum from a whole new perspective with a hot air balloon ride. Soar above the towering sandstone cliffs, vast desert landscapes, and witness a stunning sunrise or sunset. This once-in-a-lifetime adventure offers panoramic views and unforgettable memories.", + "locationName": "Wadi Rum", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "jordan", + "ref": "hot-air-balloon-ride-over-wadi-rum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_hot-air-balloon-ride-over-wadi-rum.jpg" + }, + { + "name": "Canyoning Adventure in Wadi Mujib", + "description": "Embark on a thrilling canyoning adventure through the Wadi Mujib, a stunning river canyon with cascading waterfalls and natural pools. Hike, swim, and rappel your way through this natural waterpark, surrounded by dramatic cliffs and lush vegetation. This activity is perfect for adventurous travelers seeking an adrenaline rush.", + "locationName": "Wadi Mujib", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "jordan", + "ref": "canyoning-adventure-in-wadi-mujib", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_canyoning-adventure-in-wadi-mujib.jpg" + }, + { + "name": "Stargazing in the Desert", + "description": "Escape the city lights and experience the magic of the desert night sky. Join a stargazing tour in Wadi Rum or another remote location and marvel at the Milky Way, constellations, and shooting stars. Learn about the celestial wonders from expert guides and enjoy the tranquility of the desert.", + "locationName": "Wadi Rum or other desert locations", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "jordan", + "ref": "stargazing-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_stargazing-in-the-desert.jpg" + }, + { + "name": "Visit the Jordan River Baptism Site", + "description": "Explore the Jordan River Baptism Site, a significant religious and historical landmark believed to be the place where Jesus was baptized. Visit the archaeological remains, churches, and chapels, and learn about the site's importance to Christianity. The serene atmosphere and spiritual significance make it a meaningful experience.", + "locationName": "Bethany Beyond the Jordan", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jordan", + "ref": "visit-the-jordan-river-baptism-site", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_visit-the-jordan-river-baptism-site.jpg" + }, + { + "name": "Explore the Ajloun Castle", + "description": "Discover the Ajloun Castle, a 12th-century Muslim fortress perched on a hilltop overlooking the Jordan Valley. Explore the castle's towers, courtyards, and underground passages, and learn about its role in defending against Crusader attacks. Enjoy breathtaking views of the surrounding landscapes and immerse yourself in Jordan's rich history.", + "locationName": "Ajloun", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "jordan", + "ref": "explore-the-ajloun-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/jordan_explore-the-ajloun-castle.jpg" + }, + { + "name": "Napali Coast Boat Tour", + "description": "Embark on an unforgettable journey along the majestic Napali Coast, renowned for its towering sea cliffs, hidden sea caves, and pristine beaches. Witness the dramatic beauty of this natural wonder from the comfort of a boat, with opportunities to spot marine life such as dolphins and sea turtles.", + "locationName": "Napali Coast", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "kauai", + "ref": "napali-coast-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_napali-coast-boat-tour.jpg" + }, + { + "name": "Hiking the Kalalau Trail", + "description": "Challenge yourself with a hike on the legendary Kalalau Trail, traversing 11 miles of rugged coastline along the Napali Coast. Experience breathtaking views of the Pacific Ocean, lush valleys, and cascading waterfalls. This strenuous hike is best suited for experienced adventurers.", + "locationName": "Napali Coast State Wilderness Park", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "kauai", + "ref": "hiking-the-kalalau-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_hiking-the-kalalau-trail.jpg" + }, + { + "name": "Snorkeling at Tunnels Beach", + "description": "Discover the vibrant underwater world at Tunnels Beach, a renowned snorkeling destination. Explore the coral reefs teeming with colorful fish, sea turtles, and other marine life. The calm, clear waters make it an ideal spot for beginners and experienced snorkelers alike.", + "locationName": "Tunnels Beach", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "kauai", + "ref": "snorkeling-at-tunnels-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_snorkeling-at-tunnels-beach.jpg" + }, + { + "name": "Waimea Canyon State Park", + "description": "Explore the vast and awe-inspiring Waimea Canyon, often referred to as the \"Grand Canyon of the Pacific.\" Hike along the canyon rim, enjoying panoramic views of its colorful layers and cascading waterfalls. Visit the Koke'e State Park nearby for additional hiking trails and scenic overlooks.", + "locationName": "Waimea Canyon State Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kauai", + "ref": "waimea-canyon-state-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_waimea-canyon-state-park.jpg" + }, + { + "name": "Kayaking the Wailua River", + "description": "Embark on a peaceful kayaking adventure along the Wailua River, paddling through lush rainforests and past ancient Hawaiian temples. Explore hidden waterfalls, secret swimming holes, and the Fern Grotto, a natural lava cave adorned with hanging ferns.", + "locationName": "Wailua River", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "kauai", + "ref": "kayaking-the-wailua-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_kayaking-the-wailua-river.jpg" + }, + { + "name": "Poipu Beach Park", + "description": "Spend a relaxing day at Poipu Beach Park, known for its calm waters, golden sand, and excellent swimming conditions. The protected bay is perfect for families with young children, and lifeguards are on duty for added safety. Enjoy sunbathing, building sandcastles, or simply soaking up the stunning ocean views.", + "locationName": "Poipu Beach Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "kauai", + "ref": "poipu-beach-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_poipu-beach-park.jpg" + }, + { + "name": "Kilauea Lighthouse and Wildlife Refuge", + "description": "Embark on a scenic drive to the Kilauea Lighthouse and Wildlife Refuge, perched on a rugged clifftop. Witness breathtaking panoramic views of the coastline and observe native seabirds like albatrosses, boobies, and shearwaters. Explore the historic lighthouse and learn about its fascinating maritime history.", + "locationName": "Kilauea Point National Wildlife Refuge", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kauai", + "ref": "kilauea-lighthouse-and-wildlife-refuge", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_kilauea-lighthouse-and-wildlife-refuge.jpg" + }, + { + "name": "Spouting Horn Blowhole", + "description": "Marvel at the natural wonder of the Spouting Horn Blowhole, a unique coastal formation where waves crash into a lava tube, creating a geyser-like eruption of water. Capture stunning photos of the powerful spray and enjoy the dramatic ocean scenery. Browse the nearby shops for local crafts and souvenirs.", + "locationName": "Spouting Horn", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "kauai", + "ref": "spouting-horn-blowhole", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_spouting-horn-blowhole.jpg" + }, + { + "name": "Hanalei Valley Lookout", + "description": "Take a scenic drive to the Hanalei Valley Lookout and be captivated by the breathtaking panoramic vistas of the lush taro fields, meandering rivers, and the majestic mountains in the distance. Capture postcard-worthy photos and immerse yourself in the serene beauty of Kauai's countryside.", + "locationName": "Hanalei Valley Lookout", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "kauai", + "ref": "hanalei-valley-lookout", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_hanalei-valley-lookout.jpg" + }, + { + "name": "Mountain Tubing Adventure", + "description": "Embark on a thrilling mountain tubing adventure through Kauai's historic irrigation system. Float down gentle waterways, passing through lush landscapes and tunnels, and enjoy the unique perspective of the island's interior. This family-friendly activity offers a fun and refreshing way to experience Kauai's natural beauty.", + "locationName": "Lihue Plantation", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "kauai", + "ref": "mountain-tubing-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_mountain-tubing-adventure.jpg" + }, + { + "name": "Ziplining through the Rainforest", + "description": "Soar through the lush Kauai rainforest on a thrilling zipline adventure. Experience breathtaking views of waterfalls, valleys, and the Pacific Ocean as you fly through the air on multiple ziplines. This exhilarating activity is perfect for adrenaline seekers and nature enthusiasts alike.", + "locationName": "Multiple locations throughout Kauai", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "kauai", + "ref": "ziplining-through-the-rainforest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_ziplining-through-the-rainforest.jpg" + }, + { + "name": "Luau Feast and Polynesian Show", + "description": "Immerse yourself in the vibrant culture of Hawaii at a traditional luau. Indulge in a delicious feast of Hawaiian dishes while enjoying captivating Polynesian music and dance performances. Witness the mesmerizing fire knife dance and learn about the rich history and traditions of the islands. This unforgettable experience is perfect for families and those seeking a taste of authentic Hawaiian culture.", + "locationName": "Various resorts and cultural centers", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "kauai", + "ref": "luau-feast-and-polynesian-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_luau-feast-and-polynesian-show.jpg" + }, + { + "name": "Helicopter Tour over the Napali Coast", + "description": "Embark on a breathtaking helicopter tour over the majestic Napali Coast. Witness the dramatic cliffs, cascading waterfalls, and hidden sea caves from a unique aerial perspective. This once-in-a-lifetime experience offers unparalleled views and is perfect for photography enthusiasts or anyone seeking a truly unforgettable adventure.", + "locationName": "Lihue Airport", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "kauai", + "ref": "helicopter-tour-over-the-napali-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_helicopter-tour-over-the-napali-coast.jpg" + }, + { + "name": "Explore Hanalei Town", + "description": "Wander through the charming town of Hanalei, known for its historic buildings, art galleries, and local boutiques. Discover unique souvenirs, indulge in delicious food at local cafes, or simply relax and soak up the laid-back atmosphere. This picturesque town offers a glimpse into the island's local culture and is perfect for a leisurely afternoon stroll.", + "locationName": "Hanalei Town", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kauai", + "ref": "explore-hanalei-town", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_explore-hanalei-town.jpg" + }, + { + "name": "Sunset Catamaran Cruise", + "description": "Sail along the stunning Kauai coastline on a romantic sunset catamaran cruise. Enjoy breathtaking views of the setting sun as you sip on tropical cocktails and savor delicious appetizers. This relaxing and picturesque experience is perfect for couples or anyone looking to unwind and enjoy the beauty of the island.", + "locationName": "Multiple locations along the coast", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "kauai", + "ref": "sunset-catamaran-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_sunset-catamaran-cruise.jpg" + }, + { + "name": "Horseback Riding Adventure", + "description": "Saddle up for an unforgettable horseback riding adventure through Kauai's lush interior. Explore hidden trails, traverse rolling hills, and soak in breathtaking views of the island's diverse landscapes. Whether you're an experienced rider or a beginner, this activity offers a unique and immersive way to connect with Kauai's natural beauty.", + "locationName": "Silver Falls Ranch or Princeville Ranch", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "kauai", + "ref": "horseback-riding-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_horseback-riding-adventure.jpg" + }, + { + "name": "Secret Beach Exploration", + "description": "Embark on a quest to discover Kauai's hidden gems – its secret beaches. Hike through lush jungles, navigate rocky coastlines, and be rewarded with secluded stretches of sand where you can relax, swim, and soak up the tranquility away from the crowds. Some popular options include Mahaulepu Heritage Trail and Polihale State Park.", + "locationName": "Mahaulepu Heritage Trail or Polihale State Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "kauai", + "ref": "secret-beach-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_secret-beach-exploration.jpg" + }, + { + "name": "Koke'e State Park Hiking and Scenic Drives", + "description": "Immerse yourself in the stunning landscapes of Koke'e State Park. Hike through diverse trails offering panoramic views of the Napali Coast, Waimea Canyon, and lush valleys. Alternatively, embark on a scenic drive along Koke'e Road, stopping at lookout points to capture breathtaking vistas and witness the island's natural wonders.", + "locationName": "Koke'e State Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "kauai", + "ref": "koke-e-state-park-hiking-and-scenic-drives", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_koke-e-state-park-hiking-and-scenic-drives.jpg" + }, + { + "name": "Attend a Farmers Market", + "description": "Immerse yourself in Kauai's local culture and flavors by visiting one of the island's vibrant farmers markets. Browse through stalls overflowing with fresh produce, tropical fruits, handcrafted goods, and unique souvenirs. Engage with local farmers and artisans, savor delicious food, and experience the island's authentic charm.", + "locationName": "Various locations throughout Kauai", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "kauai", + "ref": "attend-a-farmers-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_attend-a-farmers-market.jpg" + }, + { + "name": "Sunset at Hanalei Bay", + "description": "Witness the magic of a Hawaiian sunset at Hanalei Bay. Relax on the golden sand, take a leisurely stroll along the shore, or paddle out on a kayak to enjoy the breathtaking views of the sky ablaze with colors. Capture unforgettable photos and create lasting memories of this iconic Kauai experience.", + "locationName": "Hanalei Bay", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "kauai", + "ref": "sunset-at-hanalei-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kauai_sunset-at-hanalei-bay.jpg" + }, + { + "name": "Masai Mara National Reserve Safari", + "description": "Embark on an exhilarating game drive through the iconic Masai Mara National Reserve, renowned for its abundance of wildlife. Witness the breathtaking spectacle of the Great Migration, where millions of wildebeest and zebras traverse the plains. Spot majestic lions, graceful giraffes, powerful elephants, and elusive leopards in their natural habitat.", + "locationName": "Masai Mara National Reserve", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "kenya", + "ref": "masai-mara-national-reserve-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_masai-mara-national-reserve-safari.jpg" + }, + { + "name": "Amboseli National Park Elephant Encounter", + "description": "Venture into Amboseli National Park, famous for its large elephant herds and breathtaking views of Mount Kilimanjaro. Observe these gentle giants as they roam freely against the backdrop of Africa's highest peak. Capture incredible photos and learn about elephant conservation efforts.", + "locationName": "Amboseli National Park", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "kenya", + "ref": "amboseli-national-park-elephant-encounter", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_amboseli-national-park-elephant-encounter.jpg" + }, + { + "name": "Lake Nakuru Flamingo Spectacle", + "description": "Visit Lake Nakuru National Park, a haven for birdwatchers. Marvel at the mesmerizing sight of thousands of pink flamingos flocking to the lake's alkaline waters. Explore the park's diverse ecosystems, home to rhinos, lions, giraffes, and a variety of bird species.", + "locationName": "Lake Nakuru National Park", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "kenya", + "ref": "lake-nakuru-flamingo-spectacle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_lake-nakuru-flamingo-spectacle.jpg" + }, + { + "name": "Maasai Village Cultural Immersion", + "description": "Immerse yourself in the vibrant culture of the Maasai people. Visit a traditional Maasai village and interact with the locals. Learn about their customs, traditions, and way of life. Witness their impressive jumping dances and intricate beadwork.", + "locationName": "Maasai Village", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kenya", + "ref": "maasai-village-cultural-immersion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_maasai-village-cultural-immersion.jpg" + }, + { + "name": "Nairobi City Exploration", + "description": "Discover the bustling capital city of Nairobi. Visit the Karen Blixen Museum, the former home of the famous author of 'Out of Africa.' Explore the Nairobi National Museum, showcasing Kenya's rich history and culture. Indulge in the city's vibrant nightlife and diverse culinary scene.", + "locationName": "Nairobi City", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "kenya", + "ref": "nairobi-city-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_nairobi-city-exploration.jpg" + }, + { + "name": "Mount Kenya Climbing Expedition", + "description": "Embark on a thrilling adventure to conquer the majestic Mount Kenya, Africa's second-highest peak. Hike through diverse ecosystems, from lush rainforests to alpine meadows, and witness breathtaking panoramic views from the summit. This challenging yet rewarding experience is perfect for adventurous souls seeking an unforgettable climb.", + "locationName": "Mount Kenya National Park", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "kenya", + "ref": "mount-kenya-climbing-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_mount-kenya-climbing-expedition.jpg" + }, + { + "name": "Diani Beach Relaxation", + "description": "Unwind on the pristine sands of Diani Beach, a tropical paradise along Kenya's coastline. Soak up the sun, swim in the crystal-clear waters, and indulge in water sports like snorkeling and diving. With its luxurious resorts, vibrant nightlife, and stunning coral reefs, Diani Beach offers the perfect blend of relaxation and adventure.", + "locationName": "Diani Beach", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "kenya", + "ref": "diani-beach-relaxation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_diani-beach-relaxation.jpg" + }, + { + "name": "Lamu Island Cultural Exploration", + "description": "Step back in time on Lamu Island, a UNESCO World Heritage Site known for its rich history and Swahili culture. Explore the narrow streets of Lamu Old Town, visit ancient mosques and fortresses, and witness the traditional dhow sailing boats. Immerse yourself in the island's unique blend of African, Arabic, and Indian influences.", + "locationName": "Lamu Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kenya", + "ref": "lamu-island-cultural-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_lamu-island-cultural-exploration.jpg" + }, + { + "name": "Tsavo National Park Rhino Tracking", + "description": "Embark on a thrilling rhino tracking experience in Tsavo National Park, home to one of the largest populations of black rhinos in Kenya. Join experienced guides on a bush walk to observe these magnificent creatures in their natural habitat and learn about conservation efforts to protect them. This unique adventure offers an unforgettable encounter with endangered wildlife.", + "locationName": "Tsavo National Park", + "duration": 7, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "kenya", + "ref": "tsavo-national-park-rhino-tracking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_tsavo-national-park-rhino-tracking.jpg" + }, + { + "name": "Hot Air Balloon Safari over the Masai Mara", + "description": "Experience the breathtaking beauty of the Masai Mara from a unique perspective with a hot air balloon safari. Ascend at dawn and witness the stunning sunrise over the vast savanna, spotting wildlife like lions, elephants, and giraffes from above. This unforgettable experience offers a serene and magical way to witness the wonders of Kenya's wildlife.", + "locationName": "Masai Mara National Reserve", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "kenya", + "ref": "hot-air-balloon-safari-over-the-masai-mara", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_hot-air-balloon-safari-over-the-masai-mara.jpg" + }, + { + "name": "Samburu National Reserve Cultural Safari", + "description": "Explore the Samburu National Reserve, known for its unique wildlife and rich cultural heritage. Encounter the Samburu people, a semi-nomadic tribe with vibrant traditions and distinctive attire. Learn about their customs, way of life, and connection to the land. Observe Grevy's zebras, reticulated giraffes, and other rare species that thrive in this arid region.", + "locationName": "Samburu National Reserve", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "kenya", + "ref": "samburu-national-reserve-cultural-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_samburu-national-reserve-cultural-safari.jpg" + }, + { + "name": "Kisite-Mpunguti Marine National Park Snorkeling Adventure", + "description": "Discover the vibrant underwater world of Kisite-Mpunguti Marine National Park. Embark on a snorkeling adventure to encounter colorful coral reefs, diverse fish species, and even dolphins and sea turtles. Explore the marine ecosystem and enjoy the crystal-clear waters of the Indian Ocean.", + "locationName": "Kisite-Mpunguti Marine National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kenya", + "ref": "kisite-mpunguti-marine-national-park-snorkeling-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_kisite-mpunguti-marine-national-park-snorkeling-adventure.jpg" + }, + { + "name": "Hike and Camp on Mount Longonot", + "description": "Embark on a challenging yet rewarding hike to the summit of Mount Longonot, a dormant volcano with breathtaking views of the Great Rift Valley. Explore the crater rim and enjoy panoramic landscapes. Camp overnight under the stars and experience the serenity of the Kenyan wilderness.", + "locationName": "Mount Longonot National Park", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "kenya", + "ref": "hike-and-camp-on-mount-longonot", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_hike-and-camp-on-mount-longonot.jpg" + }, + { + "name": "Explore the Karen Blixen Museum", + "description": "Step back in time at the Karen Blixen Museum, the former home of the famous Danish author of \"Out of Africa.\" Explore the colonial farmhouse and its surrounding gardens, gaining insights into Kenya's colonial history and Blixen's life and literary works.", + "locationName": "Nairobi", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "kenya", + "ref": "explore-the-karen-blixen-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_explore-the-karen-blixen-museum.jpg" + }, + { + "name": "White Water Rafting on Tana River", + "description": "Experience the thrill of white water rafting down the Tana River, Kenya's longest river. Navigate through exhilarating rapids, surrounded by stunning landscapes and lush vegetation. This adventure offers an adrenaline-pumping experience for both beginners and seasoned rafters.", + "locationName": "Tana River", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "kenya", + "ref": "white-water-rafting-on-tana-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_white-water-rafting-on-tana-river.jpg" + }, + { + "name": "Explore the Aberdare Ranges", + "description": "Discover the breathtaking beauty of the Aberdare Ranges, a mountain range with diverse ecosystems, including bamboo forests, moorlands, and waterfalls. Hike through scenic trails, encounter unique wildlife like the black rhino and bongo antelope, and enjoy stunning views of the surrounding landscapes.", + "locationName": "Aberdare National Park", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kenya", + "ref": "explore-the-aberdare-ranges", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_explore-the-aberdare-ranges.jpg" + }, + { + "name": "Birdwatching in Kakamega Forest", + "description": "Immerse yourself in the vibrant world of birds at Kakamega Forest, known for its exceptional avian diversity. Spot rare and endemic species, including the Great Blue Turaco and the Turner's Eremomela, while enjoying the tranquility of this ancient rainforest.", + "locationName": "Kakamega Forest National Reserve", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "kenya", + "ref": "birdwatching-in-kakamega-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_birdwatching-in-kakamega-forest.jpg" + }, + { + "name": "Relax at Lake Naivasha", + "description": "Unwind by the serene shores of Lake Naivasha, a freshwater lake teeming with birdlife and surrounded by lush landscapes. Take a boat ride to Crescent Island, enjoy a nature walk, or simply relax and soak up the peaceful atmosphere.", + "locationName": "Lake Naivasha", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kenya", + "ref": "relax-at-lake-naivasha", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_relax-at-lake-naivasha.jpg" + }, + { + "name": "Stargazing in the Chyulu Hills", + "description": "Escape the city lights and experience the magic of stargazing in the Chyulu Hills. With minimal light pollution, the night sky comes alive with countless stars, offering a breathtaking spectacle for astronomy enthusiasts and nature lovers alike.", + "locationName": "Chyulu Hills", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "kenya", + "ref": "stargazing-in-the-chyulu-hills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kenya_stargazing-in-the-chyulu-hills.jpg" + }, + { + "name": "Temple Hopping in the Eastern Mountains", + "description": "Embark on a serene journey through Kyoto's eastern mountains, home to some of Japan's most iconic temples. Explore the tranquil Kiyomizu-dera Temple with its stunning wooden stage and panoramic city views. Wander through the moss-covered gardens of Nanzen-ji Temple, and discover the vibrant red gates of Fushimi Inari-taisha Shrine, winding up the hillside.", + "locationName": "Eastern Kyoto", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "kyoto", + "ref": "temple-hopping-in-the-eastern-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_temple-hopping-in-the-eastern-mountains.jpg" + }, + { + "name": "Geisha Spotting and Cultural Immersion in Gion", + "description": "Step back in time and immerse yourself in the enchanting district of Gion. Wander through its narrow streets lined with traditional wooden machiya houses, and keep an eye out for the elusive geishas in their elegant kimonos. Experience a traditional tea ceremony, learn about the geisha culture, and enjoy a kaiseki dinner for a truly authentic cultural immersion.", + "locationName": "Gion District", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "kyoto", + "ref": "geisha-spotting-and-cultural-immersion-in-gion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_geisha-spotting-and-cultural-immersion-in-gion.jpg" + }, + { + "name": "Arashiyama Bamboo Grove and Sagano Scenic Railway", + "description": "Escape the city bustle and venture to the enchanting Arashiyama Bamboo Grove. Stroll through the towering bamboo stalks, creating a surreal and peaceful atmosphere. Afterward, take a scenic train ride on the Sagano Romantic Train, enjoying breathtaking views of the Hozugawa River and the surrounding mountains.", + "locationName": "Arashiyama", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "kyoto", + "ref": "arashiyama-bamboo-grove-and-sagano-scenic-railway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_arashiyama-bamboo-grove-and-sagano-scenic-railway.jpg" + }, + { + "name": "Nishiki Market Foodie Adventure", + "description": "Indulge your senses in the vibrant Nishiki Market, known as 'Kyoto's Kitchen.' Explore the bustling stalls offering a wide array of fresh seafood, local produce, and traditional Japanese sweets. Sample delicious street food, discover unique culinary ingredients, and experience the authentic flavors of Kyoto.", + "locationName": "Nishiki Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "kyoto", + "ref": "nishiki-market-foodie-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_nishiki-market-foodie-adventure.jpg" + }, + { + "name": "Golden Pavilion and Zen Garden Serenity", + "description": "Marvel at the Golden Pavilion (Kinkaku-ji Temple), a shimmering golden structure reflected in a serene pond. Explore the surrounding zen gardens, meticulously designed to inspire tranquility and contemplation. Witness the harmony of nature and architecture, and experience a moment of peace in the heart of Kyoto.", + "locationName": "Kinkaku-ji Temple", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kyoto", + "ref": "golden-pavilion-and-zen-garden-serenity", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_golden-pavilion-and-zen-garden-serenity.jpg" + }, + { + "name": "Fushimi Inari-taisha Shrine Hike", + "description": "Embark on a captivating hike through thousands of vibrant orange torii gates that wind their way up the mountainside at Fushimi Inari-taisha Shrine. This iconic landmark offers stunning views of the city and a glimpse into the Shinto religion. **Tags:** Hiking, Sightseeing, Cultural experiences, Instagrammable", + "locationName": "Fushimi Inari-taisha Shrine", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kyoto", + "ref": "fushimi-inari-taisha-shrine-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_fushimi-inari-taisha-shrine-hike.jpg" + }, + { + "name": "Kiyomizu-dera Temple and the Philosopher's Path", + "description": "Visit the historic Kiyomizu-dera Temple, known for its wooden stage and breathtaking views of the city. Afterwards, take a leisurely stroll along the Philosopher's Path, a serene stone path lined with cherry blossom trees and traditional shops. **Tags:** Sightseeing, Cultural experiences, Relaxing, Instagrammable, Spring destination", + "locationName": "Eastern Kyoto", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kyoto", + "ref": "kiyomizu-dera-temple-and-the-philosopher-s-path", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_kiyomizu-dera-temple-and-the-philosopher-s-path.jpg" + }, + { + "name": "Pontocho Alley and Kamogawa River", + "description": "Wander through the charming Pontocho Alley, a narrow lane filled with traditional teahouses and restaurants. Enjoy a delicious meal while overlooking the picturesque Kamogawa River, especially enchanting in the evening with illuminated riverside dining. **Tags:** Food tours, Nightlife, Cultural experiences, Romantic", + "locationName": "Central Kyoto", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "kyoto", + "ref": "pontocho-alley-and-kamogawa-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_pontocho-alley-and-kamogawa-river.jpg" + }, + { + "name": "Arashiyama Monkey Park Iwatayama", + "description": "Take a short hike up Mt. Arashiyama to the Monkey Park Iwatayama, home to over 100 playful monkeys. Enjoy panoramic views of the city and the opportunity to observe these fascinating creatures in their natural habitat. **Tags:** Hiking, Wildlife watching, Family-friendly", + "locationName": "Arashiyama", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kyoto", + "ref": "arashiyama-monkey-park-iwatayama", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_arashiyama-monkey-park-iwatayama.jpg" + }, + { + "name": "Nijo Castle", + "description": "Explore the opulent Nijo Castle, a UNESCO World Heritage Site and former residence of the Tokugawa shoguns. Discover its impressive architecture, including the Ninomaru Palace with its 'nightingale floors' and stunning gardens. **Tags:** Sightseeing, Historic, Cultural experiences", + "locationName": "Central Kyoto", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "kyoto", + "ref": "nijo-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_nijo-castle.jpg" + }, + { + "name": "Tea Ceremony Experience at Ensian", + "description": "Immerse yourself in the graceful art of the Japanese tea ceremony at Ensian. Dressed in a traditional kimono, learn the intricate steps of preparing and serving matcha tea from a tea master. Experience the tranquility and cultural significance of this ancient ritual in a serene tatami-mat room.", + "locationName": "Ensian", + "duration": 1.5, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "kyoto", + "ref": "tea-ceremony-experience-at-ensian", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_tea-ceremony-experience-at-ensian.jpg" + }, + { + "name": "Kimono Forest at Randen Arashiyama Station", + "description": "Step into a magical world of color and light at the Kimono Forest. Wander through a pathway lined with hundreds of illuminated kimono-clad poles, each representing a unique pattern and design. Capture stunning photos and enjoy the enchanting atmosphere, especially beautiful in the evening.", + "locationName": "Randen Arashiyama Station", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "kyoto", + "ref": "kimono-forest-at-randen-arashiyama-station", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_kimono-forest-at-randen-arashiyama-station.jpg" + }, + { + "name": "Sake Brewery Tour and Tasting at Gekkeikan Okura Sake Museum", + "description": "Delve into the fascinating world of sake brewing with a tour of the Gekkeikan Okura Sake Museum. Learn about the history and process of sake production, explore traditional brewing tools and equipment, and enjoy a guided tasting of different sake varieties. Discover the unique flavors and cultural significance of this beloved Japanese beverage.", + "locationName": "Gekkeikan Okura Sake Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "kyoto", + "ref": "sake-brewery-tour-and-tasting-at-gekkeikan-okura-sake-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_sake-brewery-tour-and-tasting-at-gekkeikan-okura-sake-museum.jpg" + }, + { + "name": "Toei Kyoto Studio Park", + "description": "Embark on a journey through the world of Japanese cinema at Toei Kyoto Studio Park. Explore authentic film sets depicting historical periods, witness live ninja and samurai shows, and even participate in a costume experience. This interactive theme park offers a fun and engaging way to learn about Japan's rich cinematic history.", + "locationName": "Toei Kyoto Studio Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "kyoto", + "ref": "toei-kyoto-studio-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_toei-kyoto-studio-park.jpg" + }, + { + "name": "Day Trip to Nara Park", + "description": "Escape the city bustle with a day trip to Nara Park, a sprawling green space renowned for its friendly wild deer. Interact with these gentle creatures, visit the Todai-ji Temple housing a giant bronze Buddha statue, and explore the Kasuga Taisha Shrine with its thousands of stone and bronze lanterns. Enjoy a relaxing stroll through the park's serene natural beauty.", + "locationName": "Nara Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kyoto", + "ref": "day-trip-to-nara-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_day-trip-to-nara-park.jpg" + }, + { + "name": "Kifune Shrine and the Enchanting Kibune Village", + "description": "Embark on a scenic journey to the northern mountains of Kyoto and discover the mystical Kifune Shrine, nestled amidst lush greenery. Stroll through the charming Kibune village, known for its traditional ryokans with balconies overlooking the Kibune River. Indulge in a unique dining experience by enjoying nagashi-somen, where cold noodles flow down bamboo chutes.", + "locationName": "Kibune Village", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "kyoto", + "ref": "kifune-shrine-and-the-enchanting-kibune-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_kifune-shrine-and-the-enchanting-kibune-village.jpg" + }, + { + "name": "Philosophical Ponderings at Nanzen-ji Temple and Aqueduct", + "description": "Explore the expansive Nanzen-ji Temple complex, renowned for its stunning architecture and serene gardens. Marvel at the impressive brick aqueduct, a historic landmark that transported water to the city. Immerse yourself in the Zen atmosphere and contemplate the profound teachings of Buddhism.", + "locationName": "Nanzen-ji Temple", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "kyoto", + "ref": "philosophical-ponderings-at-nanzen-ji-temple-and-aqueduct", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_philosophical-ponderings-at-nanzen-ji-temple-and-aqueduct.jpg" + }, + { + "name": "Arashiyama Bamboo Grove Bike Tour", + "description": "Embark on an exciting bike tour through the enchanting Arashiyama Bamboo Grove. Cycle through the towering bamboo stalks, creating an unforgettable visual and auditory experience. Explore the surrounding areas, including the picturesque Togetsukyo Bridge and the Tenryuji Temple, at your own pace.", + "locationName": "Arashiyama Bamboo Grove", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "kyoto", + "ref": "arashiyama-bamboo-grove-bike-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_arashiyama-bamboo-grove-bike-tour.jpg" + }, + { + "name": "Pontocho Evening Food Tour", + "description": "Embark on a culinary adventure through Pontocho Alley, a historic entertainment district renowned for its traditional restaurants and teahouses. Sample a variety of authentic Kyoto dishes, from delicate kaiseki to hearty ramen, while experiencing the vibrant nightlife atmosphere.", + "locationName": "Pontocho Alley", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "kyoto", + "ref": "pontocho-evening-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_pontocho-evening-food-tour.jpg" + }, + { + "name": "Gion Corner Cultural Show", + "description": "Immerse yourself in traditional Japanese arts at the Gion Corner, a renowned venue showcasing various cultural performances. Witness the elegance of Kyomai dance, the artistry of tea ceremony, and the captivating sounds of the koto, a traditional stringed instrument.", + "locationName": "Gion Corner", + "duration": 1.5, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "kyoto", + "ref": "gion-corner-cultural-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/kyoto_gion-corner-cultural-show.jpg" + }, + { + "name": "Row a Pletna Boat to Bled Island", + "description": "Embark on a traditional pletna boat, a wooden rowboat unique to Lake Bled, and glide across the emerald waters to Bled Island. Explore the charming island, visit the Church of the Assumption, and ring the wishing bell for good luck. The picturesque views and serene atmosphere make this a truly romantic experience.", + "locationName": "Lake Bled", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-bled", + "ref": "row-a-pletna-boat-to-bled-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_row-a-pletna-boat-to-bled-island.jpg" + }, + { + "name": "Hike to Ojstrica for Panoramic Views", + "description": "Embark on a moderate hike up Ojstrica hill for breathtaking panoramic views of Lake Bled, the island, and the surrounding Julian Alps. The trail is well-maintained and offers several viewpoints along the way. Pack a picnic and enjoy the scenery from the summit, capturing stunning photos of the iconic landscape.", + "locationName": "Ojstrica Hill", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "lake-bled", + "ref": "hike-to-ojstrica-for-panoramic-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_hike-to-ojstrica-for-panoramic-views.jpg" + }, + { + "name": "Indulge in Bled Cream Cake", + "description": "Treat yourself to a slice of the famous Bled cream cake, a local delicacy known as Kremšnita. This delicious dessert features layers of crispy pastry, creamy vanilla custard, and a dusting of powdered sugar. Enjoy it at a lakeside cafe while soaking up the views and ambiance.", + "locationName": "Park Cafe", + "duration": 1, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-bled", + "ref": "indulge-in-bled-cream-cake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_indulge-in-bled-cream-cake.jpg" + }, + { + "name": "Explore Bled Castle", + "description": "Step back in time with a visit to Bled Castle, perched high on a cliff overlooking the lake. Explore the medieval architecture, museum exhibits, and castle printing works. Enjoy stunning views from the castle walls and indulge in a meal at the castle restaurant for a truly memorable experience.", + "locationName": "Bled Castle", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-bled", + "ref": "explore-bled-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_explore-bled-castle.jpg" + }, + { + "name": "Relax at the Thermal Spa", + "description": "Unwind and rejuvenate at the Bled Thermal Spa, known for its healing thermal waters. Enjoy a variety of spa treatments, swim in the indoor and outdoor pools, and relax in the saunas and steam rooms. The spa offers a perfect escape for relaxation and wellness.", + "locationName": "Bled Thermal Spa", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-bled", + "ref": "relax-at-the-thermal-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_relax-at-the-thermal-spa.jpg" + }, + { + "name": "Vintgar Gorge Adventure", + "description": "Embark on a breathtaking journey through the Vintgar Gorge, a natural wonder carved by the Radovna River. Walk along wooden walkways suspended above the turquoise waters, marvel at cascading waterfalls, and feel the refreshing mist on your face. This family-friendly adventure offers stunning views and a connection with nature.", + "locationName": "Vintgar Gorge", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-bled", + "ref": "vintgar-gorge-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_vintgar-gorge-adventure.jpg" + }, + { + "name": "Biking Around the Lake", + "description": "Rent a bike and enjoy a leisurely ride around the picturesque Lake Bled. Cycle through charming villages, past lush meadows, and along the lakeshore, taking in the breathtaking scenery. Stop for a picnic lunch with panoramic views or enjoy a refreshing swim in the lake. This activity is perfect for a relaxing and active day out.", + "locationName": "Lake Bled", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-bled", + "ref": "biking-around-the-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_biking-around-the-lake.jpg" + }, + { + "name": "Stand Up Paddleboarding on the Lake", + "description": "Experience the tranquility of Lake Bled from a unique perspective with stand-up paddleboarding. Glide across the calm waters, surrounded by stunning mountain views and the iconic island church. Enjoy a peaceful workout while taking in the beauty of your surroundings.", + "locationName": "Lake Bled", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-bled", + "ref": "stand-up-paddleboarding-on-the-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_stand-up-paddleboarding-on-the-lake.jpg" + }, + { + "name": "Wine Tasting in the Countryside", + "description": "Discover the local flavors of Slovenia with a wine tasting tour in the surrounding countryside. Visit family-run wineries, learn about the winemaking process, and indulge in a variety of delicious Slovenian wines. Enjoy the scenic landscapes and charming atmosphere of the wine region.", + "locationName": "Goriska Brda or Vipava Valley", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-bled", + "ref": "wine-tasting-in-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_wine-tasting-in-the-countryside.jpg" + }, + { + "name": "Traditional Slovenian Dinner with Folk Music", + "description": "Immerse yourself in Slovenian culture with a traditional dinner accompanied by live folk music. Savor authentic dishes like Kranjska klobasa (Carniolan sausage), žlikrofi (dumplings), and potica (nut roll) while enjoying the lively atmosphere and traditional music.", + "locationName": "Local restaurants or guesthouses", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-bled", + "ref": "traditional-slovenian-dinner-with-folk-music", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_traditional-slovenian-dinner-with-folk-music.jpg" + }, + { + "name": "Soar Above the Lake in a Hot Air Balloon", + "description": "Experience the breathtaking beauty of Lake Bled from a unique perspective with a hot air balloon ride. Drift silently over the emerald waters, the island church, and the surrounding mountains as you soak in the panoramic views. This unforgettable experience is perfect for a romantic occasion or a special treat.", + "locationName": "Lake Bled", + "duration": 1.5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "lake-bled", + "ref": "soar-above-the-lake-in-a-hot-air-balloon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_soar-above-the-lake-in-a-hot-air-balloon.jpg" + }, + { + "name": "Canyoning Adventure in the Bohinj Valley", + "description": "Embark on an exhilarating canyoning adventure in the nearby Bohinj Valley. Rappel down waterfalls, slide down natural water slides, and jump into crystal-clear pools. This action-packed activity is perfect for thrill-seekers and nature lovers.", + "locationName": "Bohinj Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-bled", + "ref": "canyoning-adventure-in-the-bohinj-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_canyoning-adventure-in-the-bohinj-valley.jpg" + }, + { + "name": "Visit the Beekeeping Museum", + "description": "Discover the fascinating world of Slovenian beekeeping at the Beekeeping Museum in Radovljica. Learn about the history and traditions of beekeeping, see different types of beehives, and sample delicious local honey. This educational and sweet experience is perfect for families.", + "locationName": "Radovljica", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-bled", + "ref": "visit-the-beekeeping-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_visit-the-beekeeping-museum.jpg" + }, + { + "name": "Explore the Charming Town of Radovljica", + "description": "Take a leisurely stroll through the charming medieval town of Radovljica, located just a short drive from Lake Bled. Admire the well-preserved architecture, browse the local shops, and enjoy a coffee or a meal at one of the cozy cafes. This is a perfect way to experience the local culture and atmosphere.", + "locationName": "Radovljica", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "lake-bled", + "ref": "explore-the-charming-town-of-radovljica", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_explore-the-charming-town-of-radovljica.jpg" + }, + { + "name": "Horseback Riding Through the Countryside", + "description": "Experience the beauty of the Slovenian countryside on horseback. Several riding schools around Lake Bled offer guided tours through meadows, forests, and along the Sava River. This is a relaxing and scenic way to enjoy the outdoors and connect with nature.", + "locationName": "Lake Bled surroundings", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-bled", + "ref": "horseback-riding-through-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_horseback-riding-through-the-countryside.jpg" + }, + { + "name": "Kayaking on Lake Bled", + "description": "Experience the tranquility of Lake Bled from a different perspective by kayaking on its crystal-clear waters. Rent a kayak and paddle at your own pace, enjoying the stunning views of the island, castle, and surrounding mountains. This is a perfect activity for a peaceful morning or afternoon on the water.", + "locationName": "Lake Bled", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-bled", + "ref": "kayaking-on-lake-bled", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_kayaking-on-lake-bled.jpg" + }, + { + "name": "Hiking to Mala Osojnica", + "description": "For breathtaking panoramic views of Lake Bled, embark on a hike to Mala Osojnica. The trail is moderately challenging and rewards hikers with stunning vistas of the lake, island, castle, and surrounding Julian Alps. This is a perfect activity for adventure seekers and photography enthusiasts.", + "locationName": "Mala Osojnica", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "lake-bled", + "ref": "hiking-to-mala-osojnica", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_hiking-to-mala-osojnica.jpg" + }, + { + "name": "Visit the Church of the Assumption on Bled Island", + "description": "Take a traditional Pletna boat ride to Bled Island and visit the iconic Church of the Assumption. Explore the church's history and architecture, ring the wishing bell for good luck, and enjoy the peaceful atmosphere of the island. This is a must-do activity for any visitor to Lake Bled.", + "locationName": "Bled Island", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-bled", + "ref": "visit-the-church-of-the-assumption-on-bled-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_visit-the-church-of-the-assumption-on-bled-island.jpg" + }, + { + "name": "Enjoy a Romantic Dinner with Lake Views", + "description": "Indulge in a romantic dinner at one of the many restaurants with stunning views of Lake Bled. Savor delicious Slovenian cuisine while admiring the illuminated island and castle in the evening. This is a perfect way to celebrate a special occasion or simply enjoy a memorable evening with your loved one. ", + "locationName": "Restaurants around Lake Bled", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-bled", + "ref": "enjoy-a-romantic-dinner-with-lake-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_enjoy-a-romantic-dinner-with-lake-views.jpg" + }, + { + "name": "Go Swimming or Sunbathing at Grajska Beach", + "description": "Relax and soak up the sun at Grajska Beach, the main swimming area on Lake Bled. Enjoy a refreshing swim in the clear waters, or simply lay back on the beach and admire the picturesque surroundings. This is a perfect activity for a warm summer day. ", + "locationName": "Grajska Beach", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "lake-bled", + "ref": "go-swimming-or-sunbathing-at-grajska-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-bled_go-swimming-or-sunbathing-at-grajska-beach.jpg" + }, + { + "name": "Lake Como Boat Tour", + "description": "Embark on a scenic boat tour across the glistening waters of Lake Como. Admire the opulent villas clinging to the hillsides, the charming villages dotting the shoreline, and the breathtaking mountain vistas. Opt for a private tour for a romantic experience or join a group tour to meet fellow travelers.", + "locationName": "Lake Como", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-como", + "ref": "lake-como-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_lake-como-boat-tour.jpg" + }, + { + "name": "Villa Carlotta Exploration", + "description": "Step into a world of botanical beauty at Villa Carlotta, a neoclassical villa boasting stunning gardens. Wander through the terraced landscape, marveling at vibrant flowerbeds, sculptures, and fountains. Don't miss the museum within the villa, showcasing artwork and historical artifacts.", + "locationName": "Tremezzo", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-como", + "ref": "villa-carlotta-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_villa-carlotta-exploration.jpg" + }, + { + "name": "Bellagio Stroll and Shopping", + "description": "Meander through the enchanting village of Bellagio, known as the 'Pearl of Lake Como.' Explore the narrow cobblestone streets lined with boutiques, cafes, and gelaterias. Indulge in some souvenir shopping or simply soak up the charming atmosphere of this lakeside gem.", + "locationName": "Bellagio", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-como", + "ref": "bellagio-stroll-and-shopping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_bellagio-stroll-and-shopping.jpg" + }, + { + "name": "Brunate Funicular Ride", + "description": "Take a thrilling funicular ride up to the village of Brunate, perched high above Lake Como. Enjoy panoramic views of the lake, surrounding mountains, and the city of Como below. Explore the charming village, visit the Volta Lighthouse, or simply relax at a café with a view.", + "locationName": "Como", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-como", + "ref": "brunate-funicular-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_brunate-funicular-ride.jpg" + }, + { + "name": "Culinary Delights in Varenna", + "description": "Embark on a culinary adventure in the charming village of Varenna. Savor authentic Italian dishes at lakeside restaurants, indulge in freshly made gelato, and sample local wines. Enjoy a leisurely dinner with breathtaking views of the sunset over Lake Como.", + "locationName": "Varenna", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "lake-como", + "ref": "culinary-delights-in-varenna", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_culinary-delights-in-varenna.jpg" + }, + { + "name": "Hiking in the Grigna Mountains", + "description": "Embark on a scenic hike in the Grigna Mountains, offering breathtaking panoramic views of Lake Como and the surrounding landscape. Choose from various trails catering to different skill levels, allowing you to immerse yourself in the natural beauty of the region.", + "locationName": "Grigna Mountains", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-como", + "ref": "hiking-in-the-grigna-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_hiking-in-the-grigna-mountains.jpg" + }, + { + "name": "Kayaking or Paddleboarding on the Lake", + "description": "Experience the tranquility of Lake Como by gliding across its crystal-clear waters in a kayak or on a paddleboard. Enjoy the stunning scenery, explore hidden coves, and discover the lakeside villages from a unique perspective.", + "locationName": "Lake Como", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-como", + "ref": "kayaking-or-paddleboarding-on-the-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_kayaking-or-paddleboarding-on-the-lake.jpg" + }, + { + "name": "Greenway del Lago di Como Bike Ride", + "description": "Embark on a cycling adventure along the Greenway del Lago di Como, a scenic path that follows the lake's shoreline. Explore charming villages, lush gardens, and historic sites while enjoying the fresh air and picturesque views.", + "locationName": "Greenway del Lago di Como", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-como", + "ref": "greenway-del-lago-di-como-bike-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_greenway-del-lago-di-como-bike-ride.jpg" + }, + { + "name": "Villa del Balbianello and Gardens Exploration", + "description": "Visit the enchanting Villa del Balbianello, a historic villa renowned for its stunning architecture, lush gardens, and breathtaking lake views. Explore the villa's opulent interiors and wander through the terraced gardens, offering a glimpse into the region's rich history and cultural heritage.", + "locationName": "Villa del Balbianello", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-como", + "ref": "villa-del-balbianello-and-gardens-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_villa-del-balbianello-and-gardens-exploration.jpg" + }, + { + "name": "Wine Tasting in the Valtellina Valley", + "description": "Indulge in a wine tasting experience in the Valtellina Valley, known for its terraced vineyards and production of Nebbiolo wines. Visit local wineries, learn about the winemaking process, and savor the unique flavors of the region.", + "locationName": "Valtellina Valley", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-como", + "ref": "wine-tasting-in-the-valtellina-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_wine-tasting-in-the-valtellina-valley.jpg" + }, + { + "name": "Seaplane Flight over Lake Como", + "description": "Experience the breathtaking beauty of Lake Como from a unique perspective with a scenic seaplane flight. Soar above the sparkling waters, charming villages, and majestic mountains, capturing unforgettable aerial views. This exhilarating adventure offers a truly unforgettable way to appreciate the lake's splendor.", + "locationName": "Lake Como", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "lake-como", + "ref": "seaplane-flight-over-lake-como", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_seaplane-flight-over-lake-como.jpg" + }, + { + "name": "Cooking Class with a Local Chef", + "description": "Immerse yourself in the culinary traditions of Italy by joining a cooking class led by a local chef. Learn the secrets of preparing authentic Italian dishes, from fresh pasta to regional specialties, using local ingredients. This hands-on experience is perfect for food enthusiasts and provides a delicious way to connect with the local culture.", + "locationName": "Various locations in Lake Como", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-como", + "ref": "cooking-class-with-a-local-chef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_cooking-class-with-a-local-chef.jpg" + }, + { + "name": "Explore the Castello di Vezio", + "description": "Step back in time with a visit to the Castello di Vezio, a medieval castle perched on a hilltop overlooking Varenna. Discover the castle's fascinating history, explore its ancient towers and ramparts, and enjoy panoramic views of the lake and surrounding landscape. The castle also features a falconry show, offering a unique opportunity to witness these majestic birds up close.", + "locationName": "Varenna", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-como", + "ref": "explore-the-castello-di-vezio", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_explore-the-castello-di-vezio.jpg" + }, + { + "name": "Relax at a Lakeside Spa", + "description": "Indulge in a pampering spa experience at one of the luxurious lakeside resorts. Choose from a variety of treatments, including massages, facials, and body wraps, designed to rejuvenate your body and mind. Enjoy the serene atmosphere and stunning lake views as you unwind and escape the stresses of everyday life.", + "locationName": "Various luxury hotels around Lake Como", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-como", + "ref": "relax-at-a-lakeside-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_relax-at-a-lakeside-spa.jpg" + }, + { + "name": "Take a Day Trip to Lugano, Switzerland", + "description": "Venture beyond the Italian border and explore the charming city of Lugano in Switzerland. Discover its picturesque old town, stroll along the lakeside promenade, and admire the stunning views of the Swiss Alps. Lugano offers a blend of Italian and Swiss culture, making it a delightful destination for a day trip.", + "locationName": "Lugano, Switzerland", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-como", + "ref": "take-a-day-trip-to-lugano-switzerland", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_take-a-day-trip-to-lugano-switzerland.jpg" + }, + { + "name": "Explore the Silk History of Como", + "description": "Delve into the fascinating history of silk production in Como, dating back centuries. Visit the Silk Museum to learn about the intricate process, from silkworm farming to weaving exquisite fabrics. Explore artisan workshops and boutiques to witness the creation of luxurious silk products and perhaps find a unique souvenir.", + "locationName": "Como", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-como", + "ref": "explore-the-silk-history-of-como", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_explore-the-silk-history-of-como.jpg" + }, + { + "name": "Villa Melzi Gardens and Art Museum", + "description": "Step into a world of botanical beauty and artistic treasures at Villa Melzi. Stroll through the enchanting gardens, adorned with sculptures, exotic plants, and vibrant flowers. Visit the on-site museum to admire a collection of sculptures, paintings, and historical artifacts.", + "locationName": "Bellagio", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-como", + "ref": "villa-melzi-gardens-and-art-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_villa-melzi-gardens-and-art-museum.jpg" + }, + { + "name": "Hike to the Lighthouse of Lenno", + "description": "Embark on a scenic hike to the Lighthouse of Lenno, offering breathtaking panoramic views of Lake Como and the surrounding mountains. The trail winds through lush forests and charming villages, providing a perfect blend of nature and culture. Pack a picnic and enjoy a peaceful lunch at the top.", + "locationName": "Lenno", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 1, + "destinationRef": "lake-como", + "ref": "hike-to-the-lighthouse-of-lenno", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_hike-to-the-lighthouse-of-lenno.jpg" + }, + { + "name": "Enjoy a Romantic Dinner with a View", + "description": "Indulge in a memorable dining experience at a lakeside restaurant with stunning views. Savor delicious Italian cuisine, accompanied by fine wines and impeccable service. Many restaurants offer outdoor terraces where you can soak in the romantic ambiance and admire the shimmering lake.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-como", + "ref": "enjoy-a-romantic-dinner-with-a-view", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_enjoy-a-romantic-dinner-with-a-view.jpg" + }, + { + "name": "Take a Ferry to Explore Lakeside Villages", + "description": "Hop on a ferry and embark on a journey to discover the charming villages that dot the shores of Lake Como. Each village has its own unique character, offering historic sites, quaint shops, and local restaurants. Explore the colorful houses of Varenna, the gardens of Tremezzo, or the historic center of Menaggio.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-como", + "ref": "take-a-ferry-to-explore-lakeside-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-como_take-a-ferry-to-explore-lakeside-villages.jpg" + }, + { + "name": "Conquer Scafell Pike", + "description": "Embark on an exhilarating hike to the summit of Scafell Pike, England's highest peak. The challenging yet rewarding trail offers stunning panoramic views of the surrounding mountains, valleys, and lakes. Pack a picnic lunch to enjoy at the top and make sure to wear sturdy hiking boots.", + "locationName": "Scafell Pike", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-district", + "ref": "conquer-scafell-pike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_conquer-scafell-pike.jpg" + }, + { + "name": "Cruise on Lake Windermere", + "description": "Experience the tranquility of Lake Windermere, the largest natural lake in England, on a scenic boat cruise. Relax and soak in the picturesque views of the surrounding fells and villages. Opt for a guided tour to learn about the history and ecology of the lake or rent a private boat for a more intimate experience.", + "locationName": "Lake Windermere", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-district", + "ref": "cruise-on-lake-windermere", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_cruise-on-lake-windermere.jpg" + }, + { + "name": "Discover Beatrix Potter's World", + "description": "Step into the enchanting world of Beatrix Potter at Hill Top, her former farmhouse and now a museum. Explore the charming rooms filled with her personal belongings and original illustrations. Afterwards, visit the World of Beatrix Potter Attraction in Bowness-on-Windermere for interactive exhibits and a delightful garden.", + "locationName": "Hill Top & World of Beatrix Potter Attraction", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-district", + "ref": "discover-beatrix-potter-s-world", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_discover-beatrix-potter-s-world.jpg" + }, + { + "name": "Indulge in Local Flavors", + "description": "Treat your taste buds to the culinary delights of the Lake District. Sample Cumberland sausage, a regional specialty, or savor a traditional roast dinner at a cozy pub. Don't miss the opportunity to try sticky toffee pudding, a delectable local dessert. Explore local farmers markets for fresh produce and artisanal cheeses.", + "locationName": "Various pubs and restaurants", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-district", + "ref": "indulge-in-local-flavors", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_indulge-in-local-flavors.jpg" + }, + { + "name": "Explore Dove Cottage and Wordsworth Museum", + "description": "Delve into the life and works of renowned poet William Wordsworth at Dove Cottage, his former residence. Explore the meticulously preserved rooms and gardens, and visit the Wordsworth Museum to discover manuscripts, letters, and other artifacts that offer insights into his creative process.", + "locationName": "Dove Cottage & Wordsworth Museum", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-district", + "ref": "explore-dove-cottage-and-wordsworth-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_explore-dove-cottage-and-wordsworth-museum.jpg" + }, + { + "name": "Go Ghyll Scrambling", + "description": "Embark on an exhilarating adventure through the Lake District's ghylls (narrow mountain streams). Ghyll scrambling involves navigating waterfalls, rocks, and pools, often requiring wading, climbing, and sometimes even jumping. It's a thrilling way to experience the rugged beauty of the region and challenge yourself physically.", + "locationName": "Various locations throughout the Lake District", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-district", + "ref": "go-ghyll-scrambling", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_go-ghyll-scrambling.jpg" + }, + { + "name": "Ride the Ravenglass and Eskdale Railway", + "description": "Take a nostalgic journey through the stunning scenery of the Lake District on the Ravenglass and Eskdale Railway. This heritage steam railway, affectionately known as La'al Ratty, winds its way through seven miles of picturesque landscapes, offering breathtaking views of mountains, valleys, and the coast. Enjoy a relaxing ride and soak in the beauty of the surrounding nature.", + "locationName": "Ravenglass", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-district", + "ref": "ride-the-ravenglass-and-eskdale-railway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_ride-the-ravenglass-and-eskdale-railway.jpg" + }, + { + "name": "Visit Muncaster Castle", + "description": "Step back in time with a visit to Muncaster Castle, a historic gem with over 800 years of history. Explore the castle's grand rooms, admire its stunning gardens, and learn about its fascinating past. The castle also features a Hawk & Owl Centre, where you can witness impressive birds of prey displays.", + "locationName": "Ravenglass", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-district", + "ref": "visit-muncaster-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_visit-muncaster-castle.jpg" + }, + { + "name": "Explore Grizedale Forest", + "description": "Immerse yourself in the natural beauty of Grizedale Forest, a haven for outdoor enthusiasts. Hike or bike through the extensive network of trails, discover hidden sculptures amidst the trees, and enjoy panoramic views of the surrounding fells. The forest also offers various adventure activities, such as Go Ape and Segway tours.", + "locationName": "Grizedale Forest", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-district", + "ref": "explore-grizedale-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_explore-grizedale-forest.jpg" + }, + { + "name": "Visit the Lakes Distillery", + "description": "Indulge in a unique experience at the Lakes Distillery, where you can discover the art of whisky, gin, and vodka production. Take a tour of the distillery, learn about the distilling process, and enjoy a tasting session of their award-winning spirits. The distillery also offers a bistro and shop, where you can savor delicious local cuisine and purchase unique souvenirs.", + "locationName": "Bassenthwaite Lake", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-district", + "ref": "visit-the-lakes-distillery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_visit-the-lakes-distillery.jpg" + }, + { + "name": "Wild Swimming in Tarn Blea", + "description": "Experience the invigorating sensation of wild swimming in the crystal-clear waters of Tarn Blea, a secluded lake nestled amidst the fells. Surrounded by dramatic scenery, this off-the-beaten-path gem offers a truly refreshing and immersive experience in nature. Take a picnic and make it a memorable day out.", + "locationName": "Tarn Blea", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "lake-district", + "ref": "wild-swimming-in-tarn-blea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_wild-swimming-in-tarn-blea.jpg" + }, + { + "name": "Stargazing at Low Gillerthwaite Field Centre", + "description": "Escape the city lights and immerse yourself in the wonders of the night sky at Low Gillerthwaite Field Centre. Join an astronomy evening and marvel at the constellations, planets, and distant galaxies through powerful telescopes. Learn about the celestial bodies and enjoy the tranquility of the dark skies.", + "locationName": "Low Gillerthwaite Field Centre", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-district", + "ref": "stargazing-at-low-gillerthwaite-field-centre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_stargazing-at-low-gillerthwaite-field-centre.jpg" + }, + { + "name": "Paddleboarding on Derwentwater", + "description": "Embark on a serene paddleboarding adventure on the tranquil waters of Derwentwater. Enjoy breathtaking views of the surrounding mountains and lush landscapes as you glide across the lake. This activity is perfect for all skill levels and offers a unique perspective of the Lake District's beauty.", + "locationName": "Derwentwater", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-district", + "ref": "paddleboarding-on-derwentwater", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_paddleboarding-on-derwentwater.jpg" + }, + { + "name": "Mountain Biking in Whinlatter Forest", + "description": "Get your adrenaline pumping with an exhilarating mountain biking experience in Whinlatter Forest. Explore the extensive network of trails, ranging from gentle family-friendly routes to challenging technical descents. Enjoy the thrill of the ride while surrounded by stunning forest scenery.", + "locationName": "Whinlatter Forest", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "lake-district", + "ref": "mountain-biking-in-whinlatter-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_mountain-biking-in-whinlatter-forest.jpg" + }, + { + "name": "Photography Tour of the Langdale Valley", + "description": "Capture the breathtaking beauty of the Lake District on a photography tour of the Langdale Valley. Led by a local expert, discover hidden gems and iconic viewpoints, learning tips and techniques to enhance your photography skills. This tour is perfect for both amateur and experienced photographers seeking to capture the essence of this stunning region.", + "locationName": "Langdale Valley", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-district", + "ref": "photography-tour-of-the-langdale-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_photography-tour-of-the-langdale-valley.jpg" + }, + { + "name": "Kayaking on Coniston Water", + "description": "Embark on a serene kayaking adventure on the tranquil waters of Coniston Water, surrounded by stunning mountain scenery. Glide along the lake's surface, exploring hidden coves and enjoying the peaceful ambiance. This activity is perfect for all skill levels and offers a unique perspective of the Lake District's beauty.", + "locationName": "Coniston Water", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-district", + "ref": "kayaking-on-coniston-water", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_kayaking-on-coniston-water.jpg" + }, + { + "name": "Rock Climbing and Abseiling in Borrowdale", + "description": "Experience the thrill of rock climbing and abseiling in the dramatic landscape of Borrowdale. With expert guides, challenge yourself on the rugged cliffs and enjoy breathtaking views of the valley. This adventurous activity is perfect for adrenaline seekers and offers a memorable experience in the heart of the Lake District.", + "locationName": "Borrowdale", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-district", + "ref": "rock-climbing-and-abseiling-in-borrowdale", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_rock-climbing-and-abseiling-in-borrowdale.jpg" + }, + { + "name": "Afternoon Tea at Lindeth Howe", + "description": "Indulge in a quintessentially English experience with a delightful afternoon tea at Lindeth Howe, a charming country house hotel once owned by Beatrix Potter. Savor a selection of delicate sandwiches, freshly baked scones, and delectable pastries, all while enjoying the elegant ambiance and stunning views of the surrounding gardens.", + "locationName": "Lindeth Howe", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "lake-district", + "ref": "afternoon-tea-at-lindeth-howe", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_afternoon-tea-at-lindeth-howe.jpg" + }, + { + "name": "Visit the World of Beatrix Potter Attraction", + "description": "Step into the enchanting world of Beatrix Potter at this family-friendly attraction. Explore interactive exhibits, charming gardens, and delightful displays that bring her beloved characters to life. This immersive experience is perfect for children and adults alike, offering a nostalgic journey through the tales of Peter Rabbit and his friends.", + "locationName": "The World of Beatrix Potter Attraction", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-district", + "ref": "visit-the-world-of-beatrix-potter-attraction", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_visit-the-world-of-beatrix-potter-attraction.jpg" + }, + { + "name": "Enjoy a scenic drive along the Kirkstone Pass", + "description": "Embark on a breathtaking drive along the Kirkstone Pass, one of the highest and most scenic mountain passes in the Lake District. Enjoy panoramic views of the surrounding valleys, rugged mountains, and shimmering lakes. Stop at viewpoints along the way to capture stunning photographs and soak in the beauty of the landscape.", + "locationName": "Kirkstone Pass", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "lake-district", + "ref": "enjoy-a-scenic-drive-along-the-kirkstone-pass", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-district_enjoy-a-scenic-drive-along-the-kirkstone-pass.jpg" + }, + { + "name": "Windsurfing on Lake Garda", + "description": "Experience the thrill of gliding across the crystal-clear waters of Lake Garda, propelled by the wind in your sails. Whether you're a seasoned windsurfer or a beginner eager to learn, the lake offers ideal conditions and stunning scenery. Rent equipment from one of the many windsurfing schools or centers dotted along the shoreline and embrace the invigorating sensation of harnessing the power of nature.", + "locationName": "Torbole", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-garda", + "ref": "windsurfing-on-lake-garda", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_windsurfing-on-lake-garda.jpg" + }, + { + "name": "Hiking in the Mountains", + "description": "Lace up your hiking boots and embark on an adventure through the breathtaking mountain trails surrounding Lake Garda. Explore the scenic landscapes of Monte Baldo, offering panoramic views of the lake and surrounding valleys. Hike amidst lush forests, discover hidden waterfalls, and breathe in the fresh mountain air. Choose from a variety of trails, catering to different fitness levels and interests.", + "locationName": "Monte Baldo", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-garda", + "ref": "hiking-in-the-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_hiking-in-the-mountains.jpg" + }, + { + "name": "Romantic Boat Tour", + "description": "Indulge in a romantic escape with a private boat tour on Lake Garda. Cruise along the shimmering waters, surrounded by picturesque villages and stunning natural landscapes. Savor a glass of local wine as you admire the breathtaking views and create unforgettable memories with your loved one. Opt for a sunset tour to witness the magical colors painting the sky as the sun dips below the horizon.", + "locationName": "Lake Garda", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-garda", + "ref": "romantic-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_romantic-boat-tour.jpg" + }, + { + "name": "Exploring Charming Towns", + "description": "Embark on a journey through the charming towns that dot the shores of Lake Garda. Wander through the narrow streets of Sirmione, known for its Scaliger Castle and thermal baths. Discover the colorful houses and lively atmosphere of Riva del Garda. Explore the historic center of Malcesine, with its medieval castle and cable car ride to Monte Baldo. Each town offers a unique blend of history, culture, and local charm.", + "locationName": "Sirmione, Riva del Garda, Malcesine", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-garda", + "ref": "exploring-charming-towns", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_exploring-charming-towns.jpg" + }, + { + "name": "Wine Tasting Experience", + "description": "Indulge in the flavors of the region with a delightful wine tasting experience. Visit local vineyards and wineries, where you can sample a variety of exquisite wines, from crisp whites to full-bodied reds. Learn about the winemaking process, the unique characteristics of the grapes grown in the area, and the passion that goes into every bottle. Pair your wine with delicious local cheeses and cured meats for a truly unforgettable culinary experience.", + "locationName": "Bardolino, Lugana", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-garda", + "ref": "wine-tasting-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_wine-tasting-experience.jpg" + }, + { + "name": "Paragliding Over Lake Garda", + "description": "Experience the thrill of soaring high above the stunning landscape of Lake Garda on a tandem paragliding flight. Enjoy breathtaking panoramic views of the crystal-clear lake, surrounding mountains, and charming villages as you glide peacefully through the air. This unforgettable adventure is perfect for thrill-seekers and nature lovers alike.", + "locationName": "Monte Baldo", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-garda", + "ref": "paragliding-over-lake-garda", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_paragliding-over-lake-garda.jpg" + }, + { + "name": "Canyoning in the Dolomites", + "description": "Embark on an exhilarating canyoning adventure in the rugged canyons of the Dolomites near Lake Garda. Descend through cascading waterfalls, rappel down rocky cliffs, and swim through crystal-clear pools. This action-packed activity is perfect for adventurous travelers seeking an adrenaline rush.", + "locationName": "Dolomites", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-garda", + "ref": "canyoning-in-the-dolomites", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_canyoning-in-the-dolomites.jpg" + }, + { + "name": "Kayaking on Lake Garda", + "description": "Explore the tranquil waters of Lake Garda at your own pace with a leisurely kayak tour. Paddle along the scenic shoreline, discover hidden coves, and enjoy the peacefulness of the surrounding nature. Kayaking is a great way to experience the beauty of the lake while getting some exercise.", + "locationName": "Various locations around the lake", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-garda", + "ref": "kayaking-on-lake-garda", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_kayaking-on-lake-garda.jpg" + }, + { + "name": "Visit the Medieval Scaliger Castle", + "description": "Step back in time with a visit to the impressive Scaliger Castle in Sirmione. Explore the historic fortress, climb the tower for panoramic views of the lake, and learn about the fascinating history of the region. This cultural experience is perfect for history buffs and architecture enthusiasts.", + "locationName": "Sirmione", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-garda", + "ref": "visit-the-medieval-scaliger-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_visit-the-medieval-scaliger-castle.jpg" + }, + { + "name": "Indulge in Gelato and Italian Cuisine", + "description": "No trip to Italy is complete without indulging in delicious gelato and authentic Italian cuisine. Explore the charming towns around Lake Garda and discover local gelaterias, trattorias, and pizzerias. Savor the flavors of fresh pasta, regional specialties, and of course, creamy gelato.", + "locationName": "Various towns around the lake", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-garda", + "ref": "indulge-in-gelato-and-italian-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_indulge-in-gelato-and-italian-cuisine.jpg" + }, + { + "name": "Swimming and Sunbathing at Baia delle Sirene", + "description": "Spend a relaxing day soaking up the Mediterranean sun and taking refreshing dips in the crystal-clear waters of Baia delle Sirene. This picturesque bay, known as the \"Bay of the Sirens,\" offers stunning views, calm waters, and a serene atmosphere, making it an ideal spot for swimming, sunbathing, and simply unwinding.", + "locationName": "Baia delle Sirene", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "lake-garda", + "ref": "swimming-and-sunbathing-at-baia-delle-sirene", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_swimming-and-sunbathing-at-baia-delle-sirene.jpg" + }, + { + "name": "Cycling Along the Lakeside", + "description": "Embark on a scenic cycling adventure along the picturesque shores of Lake Garda. Rent a bike and explore the dedicated cycling paths that wind through charming villages, offering breathtaking views of the lake and surrounding mountains. Stop for a picnic lunch amidst the vineyards or enjoy a refreshing gelato in a local cafe.", + "locationName": "Lake Garda", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-garda", + "ref": "cycling-along-the-lakeside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_cycling-along-the-lakeside.jpg" + }, + { + "name": "Visit the Vittoriale degli Italiani", + "description": "Immerse yourself in history and culture at the Vittoriale degli Italiani, the former estate of the renowned Italian poet Gabriele d'Annunzio. Explore the opulent villa, its extensive gardens, and unique museum collections, including a warship and an open-air theater. This fascinating complex offers a glimpse into the life and legacy of a prominent figure in Italian history.", + "locationName": "Gardone Riviera", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-garda", + "ref": "visit-the-vittoriale-degli-italiani", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_visit-the-vittoriale-degli-italiani.jpg" + }, + { + "name": "Take a Cooking Class", + "description": "Discover the secrets of Italian cuisine by taking a cooking class. Learn to prepare traditional dishes like fresh pasta, risotto, and regional specialties under the guidance of a local chef. Enjoy the fruits of your labor with a delicious meal paired with regional wines, and take home newfound culinary skills to impress your friends and family.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-garda", + "ref": "take-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_take-a-cooking-class.jpg" + }, + { + "name": "Explore Grotte di Catullo", + "description": "Step back in time and explore the ruins of Grotte di Catullo, an ancient Roman villa dating back to the 1st century BC. Wander through the remains of this once-luxurious residence, marvel at its impressive architecture, and enjoy panoramic views of Lake Garda. This archaeological site offers a fascinating glimpse into the region's rich history.", + "locationName": "Sirmione", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-garda", + "ref": "explore-grotte-di-catullo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_explore-grotte-di-catullo.jpg" + }, + { + "name": "Horseback Riding in the Hills", + "description": "Embark on a scenic horseback riding adventure through the rolling hills surrounding Lake Garda. Experienced guides will lead you on trails offering breathtaking views of the lake and surrounding landscapes. This activity is perfect for nature lovers and those seeking a unique way to explore the region.", + "locationName": "Various stables around Lake Garda", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-garda", + "ref": "horseback-riding-in-the-hills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_horseback-riding-in-the-hills.jpg" + }, + { + "name": "Visit Gardaland Amusement Park", + "description": "Experience thrills and excitement at Gardaland, one of Europe's most popular amusement parks. Enjoy a variety of rides, shows, and attractions suitable for all ages. From thrilling roller coasters to enchanting fantasy lands, Gardaland offers a fun-filled day for the whole family.", + "locationName": "Castelnuovo del Garda", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "lake-garda", + "ref": "visit-gardaland-amusement-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_visit-gardaland-amusement-park.jpg" + }, + { + "name": "Take a Day Trip to Verona", + "description": "Explore the romantic city of Verona, famous for being the setting of Shakespeare's Romeo and Juliet. Visit Juliet's balcony, marvel at the ancient Roman Arena, and wander through charming piazzas. Verona is a must-visit destination for history buffs and hopeless romantics alike.", + "locationName": "Verona", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-garda", + "ref": "take-a-day-trip-to-verona", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_take-a-day-trip-to-verona.jpg" + }, + { + "name": "Indulge in a Spa Day", + "description": "Unwind and rejuvenate with a luxurious spa day at one of Lake Garda's many wellness centers. Enjoy a variety of treatments, such as massages, facials, and body wraps, while taking in the serene surroundings. This is the perfect way to relax and recharge during your vacation.", + "locationName": "Various spa resorts around Lake Garda", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-garda", + "ref": "indulge-in-a-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_indulge-in-a-spa-day.jpg" + }, + { + "name": "Go on a Wine Tour and Tasting", + "description": "Discover the rich winemaking traditions of the Lake Garda region. Visit local vineyards, learn about the winemaking process, and indulge in tastings of renowned Italian wines. This experience is perfect for wine enthusiasts and those looking to savor the flavors of the region.", + "locationName": "Various wineries around Lake Garda", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-garda", + "ref": "go-on-a-wine-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-garda_go-on-a-wine-tour-and-tasting.jpg" + }, + { + "name": "Hiking the Rubicon Trail", + "description": "Embark on a breathtaking journey along the Rubicon Trail, a moderate 4.5-mile hike offering panoramic views of the lake and surrounding mountains. Discover hidden coves, cascading waterfalls, and the iconic Emerald Bay. Pack a picnic and enjoy lunch with a view.", + "locationName": "D.L. Bliss State Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-tahoe", + "ref": "hiking-the-rubicon-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_hiking-the-rubicon-trail.jpg" + }, + { + "name": "Kayaking or Paddleboarding on the Lake", + "description": "Experience the tranquility of Lake Tahoe from a kayak or paddleboard. Glide across the crystal-clear water, surrounded by stunning mountain scenery. Rent equipment from various locations around the lake and explore hidden coves or simply relax and soak up the sun.", + "locationName": "Various locations around Lake Tahoe", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-tahoe", + "ref": "kayaking-or-paddleboarding-on-the-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_kayaking-or-paddleboarding-on-the-lake.jpg" + }, + { + "name": "Scenic Gondola Ride at Heavenly Mountain Resort", + "description": "Soar above the treetops in a scenic gondola ride at Heavenly Mountain Resort. Enjoy panoramic views of Lake Tahoe and the surrounding Sierra Nevada mountains. At the observation deck, capture breathtaking photos and learn about the area's history and ecology.", + "locationName": "Heavenly Mountain Resort", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-tahoe", + "ref": "scenic-gondola-ride-at-heavenly-mountain-resort", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_scenic-gondola-ride-at-heavenly-mountain-resort.jpg" + }, + { + "name": "Exploring Emerald Bay State Park", + "description": "Discover the jewel of Lake Tahoe, Emerald Bay State Park. Visit Vikingsholm, a Scandinavian-inspired castle, and learn about its fascinating history. Hike down to the shore of Emerald Bay and take a dip in the refreshing water or simply relax on the beach and enjoy the views.", + "locationName": "Emerald Bay State Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-tahoe", + "ref": "exploring-emerald-bay-state-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_exploring-emerald-bay-state-park.jpg" + }, + { + "name": "Indulging in Local Cuisine and Craft Beer", + "description": "After a day of adventure, treat yourself to Lake Tahoe's culinary scene. Explore the diverse restaurants offering fresh seafood, farm-to-table dishes, and international flavors. Sample local craft beers at breweries with stunning lake views and enjoy live music at vibrant bars and pubs.", + "locationName": "Various locations around Lake Tahoe", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-tahoe", + "ref": "indulging-in-local-cuisine-and-craft-beer", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_indulging-in-local-cuisine-and-craft-beer.jpg" + }, + { + "name": "Biking the Flume Trail", + "description": "Embark on an unforgettable mountain biking experience along the Flume Trail, renowned for its breathtaking vistas of Lake Tahoe. This 14-mile singletrack trail winds through scenic forests and along dramatic cliffs, offering thrilling challenges and rewarding panoramas for intermediate to advanced riders.", + "locationName": "Flume Trail", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-tahoe", + "ref": "biking-the-flume-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_biking-the-flume-trail.jpg" + }, + { + "name": "Sunset Cruise on Lake Tahoe", + "description": "Experience the magic of Lake Tahoe at twilight with a relaxing sunset cruise. Sail across the pristine waters as the sky transforms into a canvas of vibrant colors, casting a golden glow on the surrounding mountains. Many cruises offer dinner and drinks, creating a romantic and unforgettable evening.", + "locationName": "Lake Tahoe", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "lake-tahoe", + "ref": "sunset-cruise-on-lake-tahoe", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_sunset-cruise-on-lake-tahoe.jpg" + }, + { + "name": "Exploring Vikingsholm Castle", + "description": "Step back in time with a visit to Vikingsholm, a unique Scandinavian-inspired castle nestled on the shores of Emerald Bay. Explore the historic mansion, admire its intricate architecture and period furnishings, and learn about its fascinating history. The surrounding gardens and stunning views of the bay add to the enchanting experience.", + "locationName": "Emerald Bay State Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-tahoe", + "ref": "exploring-vikingsholm-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_exploring-vikingsholm-castle.jpg" + }, + { + "name": "Stargazing at the Heavenly Mountain Observatory", + "description": "Escape the city lights and delve into the wonders of the cosmos at the Heavenly Mountain Observatory. Join a guided stargazing tour and observe celestial objects through powerful telescopes, learning about constellations, planets, and galaxies from expert astronomers. The clear mountain air and high altitude provide exceptional viewing conditions.", + "locationName": "Heavenly Mountain Resort", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-tahoe", + "ref": "stargazing-at-the-heavenly-mountain-observatory", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_stargazing-at-the-heavenly-mountain-observatory.jpg" + }, + { + "name": "Visiting the Tallac Historic Site", + "description": "Immerse yourself in the rich history of Lake Tahoe at the Tallac Historic Site. Explore the preserved estates of wealthy families from the early 20th century, including the Pope Estate, Baldwin Estate, and Valhalla Estate. Learn about the region's logging and tourism industries, and enjoy captivating views of the lake.", + "locationName": "Tallac Historic Site", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-tahoe", + "ref": "visiting-the-tallac-historic-site", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_visiting-the-tallac-historic-site.jpg" + }, + { + "name": "Horseback Riding in the Sierras", + "description": "Embark on a scenic horseback riding adventure through the picturesque trails of the Sierra Nevada mountains. Explore alpine meadows, dense forests, and enjoy breathtaking views of Lake Tahoe. Several stables in the area offer guided tours for all skill levels, making it a perfect activity for families and nature lovers.", + "locationName": "Camp Richardson Corral or Zephyr Cove Stables", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-tahoe", + "ref": "horseback-riding-in-the-sierras", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_horseback-riding-in-the-sierras.jpg" + }, + { + "name": "Rock Climbing and Bouldering", + "description": "Challenge yourself with rock climbing and bouldering at one of the many renowned climbing areas around Lake Tahoe. Whether you're a beginner or an experienced climber, you'll find routes that suit your skill level. Donner Summit, Lover's Leap, and the Tahoe Basin offer stunning natural landscapes and thrilling climbing experiences.", + "locationName": "Donner Summit, Lover's Leap, or Tahoe Basin", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "lake-tahoe", + "ref": "rock-climbing-and-bouldering", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_rock-climbing-and-bouldering.jpg" + }, + { + "name": "Hot Air Balloon Ride over Lake Tahoe", + "description": "Experience the magic of Lake Tahoe from a unique perspective with a hot air balloon ride. Soar above the crystal-clear waters and enjoy panoramic views of the surrounding mountains and forests. This unforgettable experience is perfect for a romantic getaway or a special occasion.", + "locationName": "Lake Tahoe Balloons or Tahoe Hot Air Balloons", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "lake-tahoe", + "ref": "hot-air-balloon-ride-over-lake-tahoe", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_hot-air-balloon-ride-over-lake-tahoe.jpg" + }, + { + "name": "Relaxation and Rejuvenation at a Spa Resort", + "description": "Indulge in a day of pampering and relaxation at one of Lake Tahoe's luxurious spa resorts. Choose from a variety of treatments, including massages, facials, and body wraps, while enjoying the serene ambiance and breathtaking views. Several resorts offer wellness packages with yoga classes, meditation sessions, and healthy dining options.", + "locationName": "The Ritz-Carlton, Lake Tahoe or Hyatt Regency Lake Tahoe Resort, Spa and Casino", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "lake-tahoe", + "ref": "relaxation-and-rejuvenation-at-a-spa-resort", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_relaxation-and-rejuvenation-at-a-spa-resort.jpg" + }, + { + "name": "Gaming Fun at the Casinos", + "description": "Experience the excitement of casino gaming in South Lake Tahoe. Try your luck at slot machines, table games, or poker tournaments. The casinos also offer live entertainment, nightclubs, and fine dining restaurants, providing a vibrant nightlife scene.", + "locationName": "Harrah's Lake Tahoe or Hard Rock Hotel & Casino Lake Tahoe", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "lake-tahoe", + "ref": "gaming-fun-at-the-casinos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_gaming-fun-at-the-casinos.jpg" + }, + { + "name": "Skiing and Snowboarding at World-Class Resorts", + "description": "Experience the thrill of gliding down the slopes at renowned ski resorts like Palisades Tahoe, Heavenly, and Northstar. With diverse terrain for all skill levels, enjoy powdery snow, stunning views, and après-ski activities.", + "locationName": "Various ski resorts around Lake Tahoe", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "lake-tahoe", + "ref": "skiing-and-snowboarding-at-world-class-resorts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_skiing-and-snowboarding-at-world-class-resorts.jpg" + }, + { + "name": "Scenic Drives and Photography Tours", + "description": "Embark on a scenic drive along the lake's shoreline, capturing breathtaking vistas and iconic landmarks. Consider a photography tour to learn tips and tricks for capturing the perfect shot of Emerald Bay, Sand Harbor, or Zephyr Cove.", + "locationName": "Lake Tahoe Scenic Drive", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-tahoe", + "ref": "scenic-drives-and-photography-tours", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_scenic-drives-and-photography-tours.jpg" + }, + { + "name": "Fishing Adventures on the Lake", + "description": "Cast your line and try your luck at catching trout, salmon, or kokanee salmon. Charter a fishing boat with a local guide or enjoy a relaxing day of fishing from the shore.", + "locationName": "Various locations around Lake Tahoe", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "lake-tahoe", + "ref": "fishing-adventures-on-the-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_fishing-adventures-on-the-lake.jpg" + }, + { + "name": "Exploring the Charming Towns", + "description": "Discover the unique character of towns like Tahoe City, South Lake Tahoe, and Truckee. Explore local shops, art galleries, and historical sites, and enjoy the vibrant atmosphere of these mountain communities.", + "locationName": "Tahoe City, South Lake Tahoe, Truckee", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-tahoe", + "ref": "exploring-the-charming-towns", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_exploring-the-charming-towns.jpg" + }, + { + "name": "Snowshoeing and Cross-Country Skiing", + "description": "Venture into the winter wonderland on snowshoes or cross-country skis. Explore serene trails through snow-covered forests, enjoying the tranquility of nature and the crisp mountain air.", + "locationName": "Various trails around Lake Tahoe", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "lake-tahoe", + "ref": "snowshoeing-and-cross-country-skiing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lake-tahoe_snowshoeing-and-cross-country-skiing.jpg" + }, + { + "name": "Explore the UNESCO World Heritage City of Luang Prabang", + "description": "Step back in time and wander through the charming streets of Luang Prabang, a UNESCO World Heritage city renowned for its well-preserved architecture, gilded temples, and serene atmosphere. Visit the magnificent Wat Xieng Thong, a 16th-century temple complex adorned with intricate mosaics and carvings, and climb Mount Phousi for panoramic views of the city at sunset.", + "locationName": "Luang Prabang", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "laos", + "ref": "explore-the-unesco-world-heritage-city-of-luang-prabang", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_explore-the-unesco-world-heritage-city-of-luang-prabang.jpg" + }, + { + "name": "Kayak Down the Mekong River", + "description": "Embark on a scenic kayaking adventure down the Mekong River, the lifeblood of Laos. Paddle through tranquil waters, passing by lush landscapes, traditional villages, and hidden caves. Opt for a half-day or full-day trip, and immerse yourself in the natural beauty and local life along the riverbanks.", + "locationName": "Mekong River", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "laos", + "ref": "kayak-down-the-mekong-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_kayak-down-the-mekong-river.jpg" + }, + { + "name": "Discover the Kuang Si Falls", + "description": "Escape to the breathtaking Kuang Si Falls, a multi-tiered waterfall cascading through turquoise pools in the heart of the jungle. Take a refreshing dip in the cool waters, hike to the top of the falls for stunning views, and visit the nearby bear rescue center.", + "locationName": "Kuang Si Falls", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "laos", + "ref": "discover-the-kuang-si-falls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_discover-the-kuang-si-falls.jpg" + }, + { + "name": "Immerse Yourself in Local Culture at a Traditional Village", + "description": "Venture beyond the tourist trail and visit a traditional Laotian village to experience authentic local life. Learn about traditional crafts like weaving and pottery, sample delicious home-cooked Lao cuisine, and engage with friendly villagers to gain insights into their customs and traditions.", + "locationName": "Various villages near Luang Prabang or Vang Vieng", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "laos", + "ref": "immerse-yourself-in-local-culture-at-a-traditional-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_immerse-yourself-in-local-culture-at-a-traditional-village.jpg" + }, + { + "name": "Trekking in the Northern Mountains", + "description": "Embark on a multi-day trek through the remote mountains of northern Laos, home to diverse ethnic minority groups and breathtaking landscapes. Experience the thrill of hiking through lush jungles, rice paddies, and traditional villages, while encountering unique cultures and breathtaking vistas.", + "locationName": "Northern Laos (Luang Namtha, Muang Sing)", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "laos", + "ref": "trekking-in-the-northern-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_trekking-in-the-northern-mountains.jpg" + }, + { + "name": "Biking the Bolaven Plateau", + "description": "Explore the Bolaven Plateau by bicycle, a scenic region known for its coffee plantations, waterfalls, and ethnic minority villages. Enjoy the fresh air and stunning views as you pedal through rolling hills, stopping to sample delicious local coffee and immerse yourself in the unique culture of the region.", + "locationName": "Bolaven Plateau (Pakse, Tad Lo)", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "laos", + "ref": "biking-the-bolaven-plateau", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_biking-the-bolaven-plateau.jpg" + }, + { + "name": "Living Land Farm Rice Experience", + "description": "Get your hands dirty and learn about traditional rice farming at the Living Land Farm near Luang Prabang. Participate in activities like planting, harvesting, and threshing rice, gaining insight into the importance of rice cultivation in Lao culture and the sustainable practices employed by local farmers.", + "locationName": "Living Land Farm (Luang Prabang)", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "laos", + "ref": "living-land-farm-rice-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_living-land-farm-rice-experience.jpg" + }, + { + "name": "Sunset Cruise on the Mekong River", + "description": "Embark on a relaxing sunset cruise along the Mekong River, enjoying panoramic views of the city, lush landscapes, and traditional villages. Sip on refreshing drinks and savor delicious local snacks as you witness the magical transformation of the sky at dusk.", + "locationName": "Mekong River (Luang Prabang, Vientiane)", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "laos", + "ref": "sunset-cruise-on-the-mekong-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_sunset-cruise-on-the-mekong-river.jpg" + }, + { + "name": "Cooking Class and Market Tour", + "description": "Delve into the world of Lao cuisine with a cooking class and market tour. Visit a bustling local market to discover fresh ingredients and learn about traditional cooking techniques, then prepare and enjoy a delicious meal under the guidance of a skilled chef.", + "locationName": "Luang Prabang, Vientiane", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "laos", + "ref": "cooking-class-and-market-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_cooking-class-and-market-tour.jpg" + }, + { + "name": "Elephant Encounter at Mandalao Elephant Conservation", + "description": "Embark on a responsible and ethical elephant experience at Mandalao Elephant Conservation. Learn about elephant behavior, observe these gentle giants in their natural habitat, and even assist with their care during bath time. This unforgettable encounter supports the conservation and well-being of elephants in Laos.", + "locationName": "Mandalao Elephant Conservation", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "laos", + "ref": "elephant-encounter-at-mandalao-elephant-conservation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_elephant-encounter-at-mandalao-elephant-conservation.jpg" + }, + { + "name": "Alms Giving Ceremony in Luang Prabang", + "description": "Rise early to witness the sacred Buddhist tradition of alms giving in Luang Prabang. Observe saffron-clad monks walk barefoot through the streets as locals offer them sticky rice and other food. This serene and spiritual experience provides a glimpse into Lao culture and religious practices.", + "locationName": "Luang Prabang", + "duration": 1, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "laos", + "ref": "alms-giving-ceremony-in-luang-prabang", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_alms-giving-ceremony-in-luang-prabang.jpg" + }, + { + "name": "Tham Kong Lo Cave Exploration", + "description": "Venture into the depths of Tham Kong Lo, one of Southeast Asia's most impressive caves. Take a boat ride through the cave's cavernous chambers, marvel at the stunning stalactites and stalagmites, and witness the emerald-colored pool illuminated by sunlight. This adventurous experience is perfect for those seeking natural wonders.", + "locationName": "Tham Kong Lo", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "laos", + "ref": "tham-kong-lo-cave-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_tham-kong-lo-cave-exploration.jpg" + }, + { + "name": "Relaxing Herbal Sauna and Massage", + "description": "Indulge in a traditional Lao herbal sauna and massage for the ultimate relaxation experience. Enjoy the therapeutic benefits of local herbs in a steam sauna followed by a soothing massage that will rejuvenate your body and mind. This is the perfect way to unwind after a day of exploring.", + "locationName": "Various spas and wellness centers", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "laos", + "ref": "relaxing-herbal-sauna-and-massage", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_relaxing-herbal-sauna-and-massage.jpg" + }, + { + "name": "Night Market Shopping in Vientiane", + "description": "Immerse yourself in the vibrant atmosphere of the Vientiane Night Market. Browse through a wide array of handicrafts, souvenirs, textiles, and local street food. This bustling market offers a unique opportunity to experience Lao culture and find unique treasures to take home.", + "locationName": "Vientiane Night Market", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "laos", + "ref": "night-market-shopping-in-vientiane", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_night-market-shopping-in-vientiane.jpg" + }, + { + "name": "Jungle Zipline Adventure", + "description": "Soar through the lush rainforest canopy on a thrilling zipline adventure! Experience the exhilaration of flying between platforms, enjoying breathtaking views of the jungle landscape below. This activity is perfect for adrenaline seekers and nature enthusiasts looking for a unique perspective of Laos' natural beauty.", + "locationName": "Northern Laos", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "laos", + "ref": "jungle-zipline-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_jungle-zipline-adventure.jpg" + }, + { + "name": " Gibbon Experience Treehouse Stay", + "description": "Embark on an unforgettable overnight adventure in the Bokeo Nature Reserve, staying in a luxurious treehouse nestled high above the jungle floor. Trek through pristine forests, encounter gibbons and other wildlife, and wake up to the sounds of nature in this truly immersive experience.", + "locationName": "Bokeo Nature Reserve", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "laos", + "ref": "-gibbon-experience-treehouse-stay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_-gibbon-experience-treehouse-stay.jpg" + }, + { + "name": "Traditional Lao Pottery Workshop", + "description": "Get hands-on with Lao culture by participating in a traditional pottery workshop. Learn the ancient techniques of shaping clay, creating intricate designs, and firing your own unique piece of pottery to take home as a souvenir.", + "locationName": "Luang Prabang", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "laos", + "ref": "traditional-lao-pottery-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_traditional-lao-pottery-workshop.jpg" + }, + { + "name": "Beerlao Brewery Tour", + "description": "Discover the secrets behind Laos' most famous beer, Beerlao, with a visit to the brewery. Learn about the brewing process, from the selection of ingredients to bottling, and enjoy a tasting of different Beerlao varieties. This tour is perfect for beer enthusiasts and those curious about local industries.", + "locationName": "Vientiane", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "laos", + "ref": "beerlao-brewery-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_beerlao-brewery-tour.jpg" + }, + { + "name": "Explore the Plain of Jars", + "description": "Journey to the mysterious Plain of Jars in Xieng Khouang Province, a unique archaeological site with thousands of ancient stone jars scattered across the landscape. Learn about the theories surrounding their origin and purpose, and marvel at the breathtaking scenery of the surrounding plains.", + "locationName": "Xieng Khouang Province", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "laos", + "ref": "explore-the-plain-of-jars", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/laos_explore-the-plain-of-jars.jpg" + }, + { + "name": "Hike to Reinebringen's Peak", + "description": "Embark on a challenging yet rewarding hike to the summit of Reinebringen, offering panoramic views of the Reinefjord, fishing villages, and surrounding mountains. Capture stunning photos of the iconic Lofoten landscape.", + "locationName": "Reinebringen Mountain", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "lofoten-islands", + "ref": "hike-to-reinebringen-s-peak", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_hike-to-reinebringen-s-peak.jpg" + }, + { + "name": "Kayak in the Fjords", + "description": "Paddle through the tranquil waters of the Lofoten fjords, surrounded by towering cliffs and dramatic scenery. Explore hidden coves, observe marine life, and experience the serenity of the Arctic wilderness.", + "locationName": "Nusfjord or Reinefjord", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lofoten-islands", + "ref": "kayak-in-the-fjords", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_kayak-in-the-fjords.jpg" + }, + { + "name": "Visit the Lofotr Viking Museum", + "description": "Step back in time at the Lofotr Viking Museum, featuring a reconstructed Viking longhouse and exhibits showcasing Viking history, culture, and daily life. Participate in interactive activities and learn about the fascinating legacy of the Vikings in the Lofoten Islands.", + "locationName": "Borg", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lofoten-islands", + "ref": "visit-the-lofotr-viking-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_visit-the-lofotr-viking-museum.jpg" + }, + { + "name": "Chase the Northern Lights", + "description": "Experience the magical spectacle of the Aurora Borealis dancing across the night sky. Join a guided tour or find a secluded spot away from light pollution for optimal viewing. Witness the vibrant colors and ethereal beauty of this natural phenomenon.", + "locationName": "Various locations away from light pollution", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "lofoten-islands", + "ref": "chase-the-northern-lights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_chase-the-northern-lights.jpg" + }, + { + "name": "Indulge in Fresh Seafood", + "description": "Savor the taste of the freshest seafood at local restaurants or fishing villages. Sample Lofoten's specialties, such as stockfish, cod, salmon, and Arctic char. Enjoy the unique culinary experience of the islands.", + "locationName": "Various restaurants and villages", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lofoten-islands", + "ref": "indulge-in-fresh-seafood", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_indulge-in-fresh-seafood.jpg" + }, + { + "name": "Go on a RIB Boat Safari", + "description": "Embark on a thrilling RIB (rigid inflatable boat) safari through the stunning Lofoten archipelago. Zip across the waves, feeling the wind in your hair as you explore hidden coves, dramatic cliffs, and encounter diverse marine life like seals, sea eagles, and even whales.", + "locationName": "Various locations around the islands", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lofoten-islands", + "ref": "go-on-a-rib-boat-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_go-on-a-rib-boat-safari.jpg" + }, + { + "name": "Take a Photography Tour", + "description": "Capture the breathtaking beauty of the Lofoten Islands on a guided photography tour. Learn tips and tricks from a professional photographer while visiting iconic locations like Hamnøy, Reine, and Sakrisøy. Capture the perfect shot of charming fishing villages, dramatic mountains, and the mesmerizing Northern Lights (depending on the season).", + "locationName": "Various locations around the islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lofoten-islands", + "ref": "take-a-photography-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_take-a-photography-tour.jpg" + }, + { + "name": "Visit the Lofoten Aquarium", + "description": "Discover the fascinating marine life of the Arctic at the Lofoten Aquarium. Observe playful seals, colorful fish, and other intriguing creatures native to the region. Learn about the ecosystem of the Lofoten Islands and the importance of ocean conservation.", + "locationName": "Kabelvåg", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "lofoten-islands", + "ref": "visit-the-lofoten-aquarium", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_visit-the-lofoten-aquarium.jpg" + }, + { + "name": "Experience the Magic of the Lofoten International Art Festival", + "description": "Immerse yourself in the vibrant art scene of the Lofoten Islands during the annual Lofoten International Art Festival (LIAF). Explore exhibitions showcasing contemporary art from local and international artists, attend workshops, and enjoy cultural events in unique venues across the islands.", + "locationName": "Various locations around the islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lofoten-islands", + "ref": "experience-the-magic-of-the-lofoten-international-art-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_experience-the-magic-of-the-lofoten-international-art-festival.jpg" + }, + { + "name": "Go Fishing with Local Fishermen", + "description": "Embark on an authentic fishing adventure with experienced local fishermen. Learn traditional fishing techniques, try your hand at catching cod, haddock, or other species, and experience the thrill of reeling in your own fresh seafood.", + "locationName": "Various locations around the islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lofoten-islands", + "ref": "go-fishing-with-local-fishermen", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_go-fishing-with-local-fishermen.jpg" + }, + { + "name": "Go horseback riding on the beach", + "description": "Experience the thrill of horseback riding along the pristine beaches of the Lofoten Islands. Feel the wind in your hair as you gallop across the sand, taking in the breathtaking views of the surrounding mountains and ocean. Several local stables offer guided tours for all skill levels, making it a perfect activity for families or solo travelers.", + "locationName": "Gimsøya Island", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lofoten-islands", + "ref": "go-horseback-riding-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_go-horseback-riding-on-the-beach.jpg" + }, + { + "name": "Explore charming fishing villages", + "description": "Wander through the picturesque fishing villages that dot the Lofoten Islands. Each village boasts its unique charm, with colorful wooden houses, traditional fishing boats, and friendly locals. Visit Henningsvær, known as the Venice of the North, or Nusfjord, a UNESCO World Heritage Site, to experience the authentic culture and history of the islands. ", + "locationName": "Henningsvær, Nusfjord", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "lofoten-islands", + "ref": "explore-charming-fishing-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_explore-charming-fishing-villages.jpg" + }, + { + "name": "Take a scenic bike ride", + "description": "Embark on a scenic bike ride along the National Tourist Route Lofoten, which stretches across the archipelago. Enjoy the fresh air and stunning landscapes as you pedal past towering mountains, dramatic fjords, and charming villages. Rent a bike from various rental shops and choose a route that suits your fitness level and interests.", + "locationName": "National Tourist Route Lofoten", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lofoten-islands", + "ref": "take-a-scenic-bike-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_take-a-scenic-bike-ride.jpg" + }, + { + "name": "Visit the Lofoten War Memorial Museum", + "description": "Delve into the history of World War II at the Lofoten War Memorial Museum. Learn about the German occupation of Norway and the resistance movement that fought for liberation. The museum exhibits artifacts, photographs, and documents that provide a glimpse into the wartime experiences of the Lofoten people.", + "locationName": "Svolvær", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "lofoten-islands", + "ref": "visit-the-lofoten-war-memorial-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_visit-the-lofoten-war-memorial-museum.jpg" + }, + { + "name": "Indulge in local craft beer", + "description": "Discover the burgeoning craft beer scene in the Lofoten Islands. Visit local breweries like Lofotpils and Svolvær Ølkompani to sample a variety of unique and flavorful beers. Take a brewery tour to learn about the brewing process and enjoy a tasting session with fellow beer enthusiasts.", + "locationName": "Svolvær, Henningsvær", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "lofoten-islands", + "ref": "indulge-in-local-craft-beer", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_indulge-in-local-craft-beer.jpg" + }, + { + "name": "Join a Whale Watching Excursion", + "description": "Embark on an unforgettable whale watching adventure in the crystal-clear waters surrounding the Lofoten Islands. Witness majestic humpback whales, orcas, and other marine life as they breach, tail-slap, and play in their natural habitat. Knowledgeable guides provide insights into these fascinating creatures and the delicate ecosystem they inhabit.", + "locationName": "Andenes or Stø", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "lofoten-islands", + "ref": "join-a-whale-watching-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_join-a-whale-watching-excursion.jpg" + }, + { + "name": "Experience Midnight Sun Phenomenon", + "description": "During the summer months, witness the magical phenomenon of the midnight sun. The sun remains above the horizon even at midnight, casting a surreal glow over the dramatic landscapes. Hike to a scenic viewpoint, enjoy a picnic under the golden light, or simply soak in the ethereal atmosphere.", + "locationName": "Reinebringen or Ryten", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "lofoten-islands", + "ref": "experience-midnight-sun-phenomenon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_experience-midnight-sun-phenomenon.jpg" + }, + { + "name": "Go on a Sea Eagle Safari", + "description": "Embark on a thrilling sea eagle safari, where you'll witness the majestic white-tailed eagles soaring through the sky. These magnificent birds of prey are a sight to behold, and you'll have the opportunity to capture stunning photos as they hunt for fish and glide effortlessly above the rugged coastline.", + "locationName": "Trollfjord or Raftsundet", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "lofoten-islands", + "ref": "go-on-a-sea-eagle-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_go-on-a-sea-eagle-safari.jpg" + }, + { + "name": "Unwind in a Traditional Sauna", + "description": "Experience the ultimate relaxation in a traditional Norwegian sauna. Immerse yourself in the warmth and let the steam soothe your muscles and melt away stress. Many saunas offer breathtaking views of the surrounding landscapes, allowing you to unwind in complete tranquility.", + "locationName": "Various locations throughout the islands", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "lofoten-islands", + "ref": "unwind-in-a-traditional-sauna", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_unwind-in-a-traditional-sauna.jpg" + }, + { + "name": "Learn to Surf in Unstad", + "description": "Catch a wave and experience the thrill of surfing in Unstad, a renowned surfing destination. Whether you're a beginner or an experienced surfer, the pristine beaches and consistent waves offer the perfect setting to ride the waves. Surf schools and equipment rentals are readily available.", + "locationName": "Unstad Beach", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lofoten-islands", + "ref": "learn-to-surf-in-unstad", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lofoten-islands_learn-to-surf-in-unstad.jpg" + }, + { + "name": "Mount Rinjani Trekking", + "description": "Embark on an unforgettable adventure by trekking up Mount Rinjani, an active volcano and the second-highest peak in Indonesia. Witness breathtaking sunrises, camp under the stars, and take a dip in the stunning crater lake. This challenging hike offers incredible rewards for those seeking an adventurous experience.", + "locationName": "Mount Rinjani National Park", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "lombok", + "ref": "mount-rinjani-trekking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_mount-rinjani-trekking.jpg" + }, + { + "name": "Island Hopping in the Gili Islands", + "description": "Escape to paradise by exploring the three Gili Islands: Gili Trawangan, Gili Meno, and Gili Air. Each island offers a unique atmosphere, from the lively bars and restaurants of Gili Trawangan to the serene beaches and crystal-clear waters of Gili Meno. Enjoy snorkeling, diving, kayaking, or simply relaxing on the beach.", + "locationName": "Gili Islands", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lombok", + "ref": "island-hopping-in-the-gili-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_island-hopping-in-the-gili-islands.jpg" + }, + { + "name": "Surfing in Kuta Lombok", + "description": "Catch some waves in Kuta Lombok, a haven for surfers of all levels. With consistent swells and a variety of breaks, you'll find the perfect spot to ride the waves. Take a lesson from a local surf school or rent a board and explore the coastline.", + "locationName": "Kuta Lombok", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "lombok", + "ref": "surfing-in-kuta-lombok", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_surfing-in-kuta-lombok.jpg" + }, + { + "name": "Exploring Sasak Culture", + "description": "Immerse yourself in the rich culture of the Sasak people, the indigenous inhabitants of Lombok. Visit traditional villages like Sade and Rambitan to see their unique architecture, weaving techniques, and way of life. Learn about their customs and traditions, and perhaps even try some local delicacies.", + "locationName": "Sade and Rambitan villages", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lombok", + "ref": "exploring-sasak-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_exploring-sasak-culture.jpg" + }, + { + "name": "Chasing Waterfalls", + "description": "Discover the enchanting waterfalls hidden throughout Lombok. Hike through lush jungles to reach cascading falls like Sendang Gile and Tiu Kelep, where you can take a refreshing dip in the cool pools below. These natural wonders offer a perfect escape from the heat and a chance to reconnect with nature.", + "locationName": "Sendang Gile and Tiu Kelep waterfalls", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "lombok", + "ref": "chasing-waterfalls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_chasing-waterfalls.jpg" + }, + { + "name": "Scuba Diving and Snorkeling in the Gili Islands", + "description": "Dive into the crystal-clear waters of the Gili Islands and discover a vibrant underwater world teeming with marine life. Explore colorful coral reefs, swim alongside tropical fish, and encounter majestic sea turtles. Whether you're a seasoned diver or a beginner snorkeler, the Gili Islands offer unforgettable underwater adventures.", + "locationName": "Gili Islands", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "lombok", + "ref": "scuba-diving-and-snorkeling-in-the-gili-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_scuba-diving-and-snorkeling-in-the-gili-islands.jpg" + }, + { + "name": "Sunset Horseback Riding on the Beach", + "description": "Experience the magic of Lombok's coastline with a romantic horseback ride as the sun dips below the horizon. Trot along pristine beaches, feel the gentle sea breeze, and witness breathtaking views of the ocean painted in hues of orange and pink. This unforgettable experience is perfect for couples or anyone seeking a tranquil escape.", + "locationName": "Senggigi Beach or Kuta Beach", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "lombok", + "ref": "sunset-horseback-riding-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_sunset-horseback-riding-on-the-beach.jpg" + }, + { + "name": "Traditional Cooking Class", + "description": "Delve into the heart of Lombok's culinary scene with a hands-on cooking class. Learn the secrets of authentic Sasak dishes from local chefs, using fresh, aromatic ingredients. Master the art of creating flavorful curries, fragrant rice dishes, and spicy sambals. This immersive experience is a must for food enthusiasts and cultural explorers.", + "locationName": "Local villages or cooking schools", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "lombok", + "ref": "traditional-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_traditional-cooking-class.jpg" + }, + { + "name": "Visit the Narmada Water Palace", + "description": "Step back in time at the Narmada Water Palace, a historic royal garden built in the 18th century. Explore the intricate Hindu temples, serene pools, and lush gardens that once served as a summer retreat for the Balinese kings. Discover the fascinating legends and cultural significance of this architectural gem.", + "locationName": "Narmada Water Palace", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "lombok", + "ref": "visit-the-narmada-water-palace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_visit-the-narmada-water-palace.jpg" + }, + { + "name": "Explore the Traditional Markets", + "description": "Immerse yourself in the vibrant atmosphere of Lombok's traditional markets. Wander through bustling stalls overflowing with exotic fruits, aromatic spices, handcrafted textiles, and unique souvenirs. Engage with friendly locals, practice your bargaining skills, and discover the authentic flavors and crafts of the island.", + "locationName": "Mandalika Market or Pasar Seni Senggigi", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "lombok", + "ref": "explore-the-traditional-markets", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_explore-the-traditional-markets.jpg" + }, + { + "name": "Kayaking in the Mangroves", + "description": "Embark on a serene kayaking adventure through the lush mangrove forests of Sekotong. Glide through the calm waters, surrounded by the vibrant ecosystem and diverse birdlife. This eco-friendly activity allows you to connect with nature and witness the unique beauty of Lombok's coastal landscapes.", + "locationName": "Sekotong", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "lombok", + "ref": "kayaking-in-the-mangroves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_kayaking-in-the-mangroves.jpg" + }, + { + "name": "Visit the Pink Beach", + "description": "Discover the natural wonder of Pink Beach, located on the southeastern coast of Lombok. The sand gets its rosy hue from microscopic red coral fragments, creating a breathtaking spectacle. Relax on the beach, swim in the crystal-clear waters, or go snorkeling to explore the vibrant underwater world.", + "locationName": "Tangsi Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lombok", + "ref": "visit-the-pink-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_visit-the-pink-beach.jpg" + }, + { + "name": "Cycling through the Rice Paddies", + "description": "Experience the authentic charm of Lombok's countryside by taking a leisurely bike ride through the verdant rice paddies. Pedal along scenic paths, witness local farmers tending to their crops, and immerse yourself in the peaceful atmosphere of rural life. This activity offers a unique perspective on Lombok's natural beauty and cultural heritage.", + "locationName": "Tetebatu", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "lombok", + "ref": "cycling-through-the-rice-paddies", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_cycling-through-the-rice-paddies.jpg" + }, + { + "name": "Indulge in a Spa Treatment", + "description": "Escape the hustle and bustle and treat yourself to a rejuvenating spa experience. Lombok offers a variety of wellness retreats and traditional spas where you can enjoy massages, body scrubs, and other relaxing treatments. Unwind and revitalize your mind, body, and soul amidst the tranquil ambiance.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "lombok", + "ref": "indulge-in-a-spa-treatment", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_indulge-in-a-spa-treatment.jpg" + }, + { + "name": "Stargazing in the Desert", + "description": "Venture into the desert landscapes of Lombok and experience the magic of stargazing. Away from the city lights, the night sky comes alive with countless stars. Join a guided tour or find a secluded spot to marvel at the constellations and enjoy the tranquility of the desert night.", + "locationName": "South Lombok", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "lombok", + "ref": "stargazing-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_stargazing-in-the-desert.jpg" + }, + { + "name": "Learn to Weave with Sasak Artisans", + "description": "Immerse yourself in the rich cultural heritage of Lombok by learning the art of weaving from skilled Sasak artisans. Visit a traditional village and witness the intricate process of creating beautiful textiles, from dyeing threads with natural pigments to weaving them into vibrant fabrics. Try your hand at the loom and create your own unique souvenir to take home.", + "locationName": "Sasak Village", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "lombok", + "ref": "learn-to-weave-with-sasak-artisans", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_learn-to-weave-with-sasak-artisans.jpg" + }, + { + "name": "Visit the Islamic Center Mosque", + "description": "Discover the architectural beauty and spiritual significance of the Islamic Center Mosque, one of the largest and most impressive mosques in Lombok. Admire its intricate design, ornate decorations, and soaring minaret. Learn about Islamic culture and traditions, and enjoy the peaceful atmosphere of this religious landmark.", + "locationName": "Islamic Center Mosque", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "lombok", + "ref": "visit-the-islamic-center-mosque", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_visit-the-islamic-center-mosque.jpg" + }, + { + "name": "Embark on a Wildlife Safari", + "description": "Venture into the lush jungles of Lombok on a thrilling wildlife safari. Spot exotic animals like monkeys, deer, and birds in their natural habitat. Learn about the island's diverse ecosystem and conservation efforts. This adventure offers a unique opportunity to connect with nature and create unforgettable memories.", + "locationName": "Rinjani National Park or Monkey Forest", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "lombok", + "ref": "embark-on-a-wildlife-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_embark-on-a-wildlife-safari.jpg" + }, + { + "name": "Gili Islands Party Hopper Boat Trip", + "description": "Experience the vibrant nightlife of the Gili Islands with a party hopper boat trip. Cruise between the islands, enjoying stunning sunset views and lively music on board. Stop at each island to explore the beachfront bars and clubs, dance the night away, and mingle with fellow travelers. This is the perfect way to let loose and experience the energetic side of Lombok.", + "locationName": "Gili Trawangan, Gili Meno, Gili Air", + "duration": 6, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "lombok", + "ref": "gili-islands-party-hopper-boat-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_gili-islands-party-hopper-boat-trip.jpg" + }, + { + "name": "Explore the Underwater World with a Seawalker Helmet Dive", + "description": "Discover the wonders of the underwater world without needing scuba diving experience. With a seawalker helmet dive, you can walk along the seabed, surrounded by colorful fish and coral reefs. Breathe normally through the helmet as you observe the marine life up close. This unique and accessible activity is perfect for families and non-swimmers.", + "locationName": "Gili Islands or Senggigi Beach", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "lombok", + "ref": "explore-the-underwater-world-with-a-seawalker-helmet-dive", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/lombok_explore-the-underwater-world-with-a-seawalker-helmet-dive.jpg" + }, + { + "name": "Alms Giving Ceremony", + "description": "Witness the captivating daily ritual of saffron-clad monks collecting alms (food offerings) from locals at dawn. Experience the serenity and cultural significance of this tradition, offering a glimpse into the spiritual heart of Luang Prabang. Remember to dress respectfully and maintain a quiet demeanor.", + "locationName": "Streets of Luang Prabang", + "duration": 1, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "luang-prabang", + "ref": "alms-giving-ceremony", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_alms-giving-ceremony.jpg" + }, + { + "name": "Kuang Si Falls Excursion", + "description": "Embark on a refreshing journey to the turquoise cascades of Kuang Si Falls. Hike through the lush jungle, swim in the cool pools, and marvel at the natural beauty of this multi-tiered waterfall. Visit the nearby bear rescue center to encounter rescued Asiatic black bears.", + "locationName": "Kuang Si Falls", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "kuang-si-falls-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_kuang-si-falls-excursion.jpg" + }, + { + "name": "Mekong River Cruise", + "description": "Embark on a scenic cruise down the Mekong River, the lifeblood of Laos. Admire the picturesque landscapes, observe local life along the riverbanks, and visit traditional villages. Opt for a sunset cruise for a romantic experience with breathtaking views.", + "locationName": "Mekong River", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "luang-prabang", + "ref": "mekong-river-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_mekong-river-cruise.jpg" + }, + { + "name": "Mount Phousi Climb and Sunset Views", + "description": "Ascend the steps to the summit of Mount Phousi for panoramic views of Luang Prabang and the surrounding mountains. Explore Wat Chom Si, a small temple at the peak, and witness a magical sunset that paints the sky in vibrant hues.", + "locationName": "Mount Phousi", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "luang-prabang", + "ref": "mount-phousi-climb-and-sunset-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_mount-phousi-climb-and-sunset-views.jpg" + }, + { + "name": "Luang Prabang Night Market", + "description": "Immerse yourself in the vibrant atmosphere of the Luang Prabang Night Market. Browse through a diverse array of handicrafts, textiles, souvenirs, and local delicacies. This bustling market offers a glimpse into the region's artistic traditions and culinary delights.", + "locationName": "Luang Prabang Night Market", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "luang-prabang-night-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_luang-prabang-night-market.jpg" + }, + { + "name": "Traditional Lao Cooking Class", + "description": "Embark on a culinary journey with a hands-on cooking class, where you'll learn to prepare authentic Lao dishes like laap, papaya salad, and sticky rice. Discover the secrets of local ingredients and techniques, and savor the delicious results of your efforts.", + "locationName": "Luang Prabang Cooking School", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "luang-prabang", + "ref": "traditional-lao-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_traditional-lao-cooking-class.jpg" + }, + { + "name": "Pak Ou Caves Exploration", + "description": "Embark on a boat trip up the Mekong River to the Pak Ou Caves, two limestone grottoes filled with thousands of Buddha statues left by devoted pilgrims over centuries. Witness the spiritual significance of this sacred site and enjoy the scenic journey along the river.", + "locationName": "Pak Ou Caves", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "pak-ou-caves-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_pak-ou-caves-exploration.jpg" + }, + { + "name": "Living Land Rice Farm Experience", + "description": "Immerse yourself in the rural life of Laos at the Living Land Farm. Learn about traditional rice cultivation methods, participate in hands-on activities like planting and harvesting, and gain a deeper understanding of the importance of rice in Lao culture.", + "locationName": "Living Land Farm", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "living-land-rice-farm-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_living-land-rice-farm-experience.jpg" + }, + { + "name": "Royal Palace Museum", + "description": "Step back in time at the Royal Palace Museum, once home to the Lao royal family. Explore the opulent halls and chambers, admire the intricate architecture and royal artifacts, and learn about the history and cultural significance of the Lao monarchy.", + "locationName": "Royal Palace Museum", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "royal-palace-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_royal-palace-museum.jpg" + }, + { + "name": "Traditional Lao Massage", + "description": "Indulge in a relaxing and rejuvenating traditional Lao massage. Experience the unique techniques and stretches that have been passed down for generations, and let the skilled therapists ease away your tension and stress.", + "locationName": "Local spas and wellness centers", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "luang-prabang", + "ref": "traditional-lao-massage", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_traditional-lao-massage.jpg" + }, + { + "name": "Tat Kuang Si Bear Rescue Center", + "description": "Visit the Tat Kuang Si Bear Rescue Center, a sanctuary for Asiatic black bears rescued from illegal wildlife trade. Learn about conservation efforts, observe these majestic creatures in a natural habitat, and support ethical wildlife tourism.", + "locationName": "Tat Kuang Si Bear Rescue Center", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "tat-kuang-si-bear-rescue-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_tat-kuang-si-bear-rescue-center.jpg" + }, + { + "name": "Bicycle Tour Through Rural Villages", + "description": "Embark on a scenic bicycle tour through the countryside surrounding Luang Prabang. Pedal along quiet roads, passing by traditional villages, rice paddies, and local farms. Interact with friendly villagers, experience rural life, and enjoy the fresh air and beautiful landscapes.", + "locationName": "Luang Prabang Countryside", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "luang-prabang", + "ref": "bicycle-tour-through-rural-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_bicycle-tour-through-rural-villages.jpg" + }, + { + "name": "Sunset Kayak on the Mekong River", + "description": "Paddle along the tranquil Mekong River as the sun sets, casting a golden glow over the water and surrounding landscape. Enjoy breathtaking views of the city and mountains, observe local fishermen, and experience the serenity of the river at dusk.", + "locationName": "Mekong River", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "luang-prabang", + "ref": "sunset-kayak-on-the-mekong-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_sunset-kayak-on-the-mekong-river.jpg" + }, + { + "name": "Ock Pop Tok Living Crafts Centre", + "description": "Immerse yourself in the world of Lao textiles at the Ock Pop Tok Living Crafts Centre. Learn about traditional weaving techniques, natural dyeing processes, and the cultural significance of textiles in Laos. Participate in a workshop, create your own souvenir, and support local artisans.", + "locationName": "Ock Pop Tok Living Crafts Centre", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "ock-pop-tok-living-crafts-centre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_ock-pop-tok-living-crafts-centre.jpg" + }, + { + "name": "Morning Alms Giving Ceremony Participation", + "description": "Rise early and participate in the sacred Buddhist tradition of alms giving. Witness the procession of saffron-robed monks as they walk through the streets collecting offerings from locals. Immerse yourself in the spiritual atmosphere and gain a deeper understanding of Lao culture.", + "locationName": "Luang Prabang streets", + "duration": 1, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "luang-prabang", + "ref": "morning-alms-giving-ceremony-participation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_morning-alms-giving-ceremony-participation.jpg" + }, + { + "name": "Jungle Trekking and Waterfall Hiking", + "description": "Embark on a thrilling jungle trek through the lush landscapes surrounding Luang Prabang. Hike to hidden waterfalls, discover diverse flora and fauna, and immerse yourself in the natural beauty of Laos. Experienced guides will lead you through scenic trails, sharing insights about the local ecosystem and cultural significance of the area.", + "locationName": "Luang Prabang mountains", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "luang-prabang", + "ref": "jungle-trekking-and-waterfall-hiking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_jungle-trekking-and-waterfall-hiking.jpg" + }, + { + "name": "Elephant Sanctuary Visit", + "description": "Spend a heartwarming day at an ethical elephant sanctuary, where you can interact with these gentle giants in a responsible and sustainable way. Learn about elephant conservation efforts, observe their natural behaviors, and even assist with feeding and bathing them. This unforgettable experience offers a unique opportunity to connect with these majestic creatures.", + "locationName": "Mandalao Elephant Conservation", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "luang-prabang", + "ref": "elephant-sanctuary-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_elephant-sanctuary-visit.jpg" + }, + { + "name": "Traditional Weaving Workshop", + "description": "Immerse yourself in the rich textile traditions of Laos with a hands-on weaving workshop. Learn about the intricate techniques and cultural significance of Lao textiles from skilled artisans. Create your own unique piece using a traditional loom and gain a deeper appreciation for this ancient craft.", + "locationName": "Ock Pop Tok Living Crafts Centre", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "traditional-weaving-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_traditional-weaving-workshop.jpg" + }, + { + "name": "Sunset Cruise on the Nam Khan River", + "description": "Enjoy a serene sunset cruise along the tranquil Nam Khan River. Witness the golden hues of the setting sun paint the sky as you glide past picturesque landscapes and charming villages. This relaxing experience offers a different perspective of Luang Prabang's beauty and allows you to unwind amidst the peaceful ambiance.", + "locationName": "Nam Khan River", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "sunset-cruise-on-the-nam-khan-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_sunset-cruise-on-the-nam-khan-river.jpg" + }, + { + "name": "Luang Prabang Film Festival", + "description": "If you're visiting in December, don't miss the Luang Prabang Film Festival. This annual event showcases a diverse selection of Southeast Asian films, offering a unique glimpse into the region's culture and storytelling traditions. Attend screenings, workshops, and panel discussions, and immerse yourself in the world of cinema.", + "locationName": "Various venues in Luang Prabang", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 2, + "destinationRef": "luang-prabang", + "ref": "luang-prabang-film-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/luang-prabang_luang-prabang-film-festival.jpg" + }, + { + "name": "Explore Ranomafana National Park", + "description": "Embark on a guided hike through the lush rainforests of Ranomafana National Park, a haven for diverse wildlife. Spot lemurs leaping through the trees, colorful chameleons blending into the foliage, and a myriad of bird species flitting through the canopy. Immerse yourself in the sights and sounds of this pristine ecosystem, learning about the park's conservation efforts and the unique flora and fauna that call it home.", + "locationName": "Ranomafana National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "madagascar", + "ref": "explore-ranomafana-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_explore-ranomafana-national-park.jpg" + }, + { + "name": "Discover Isalo National Park's Jurassic Landscape", + "description": "Venture into Isalo National Park, renowned for its dramatic sandstone canyons, deep gorges, and unique rock formations that resemble a prehistoric landscape. Hike through the park's diverse terrain, encountering natural swimming pools, cascading waterfalls, and hidden caves. Learn about the Bara people, who inhabit the region, and their fascinating cultural traditions.", + "locationName": "Isalo National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "madagascar", + "ref": "discover-isalo-national-park-s-jurassic-landscape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_discover-isalo-national-park-s-jurassic-landscape.jpg" + }, + { + "name": "Relax on the Beaches of Nosy Be", + "description": "Escape to the idyllic island of Nosy Be, where pristine white-sand beaches meet turquoise waters. Bask in the sun, swim in the crystal-clear ocean, or try your hand at water sports like snorkeling or scuba diving to explore the vibrant coral reefs teeming with marine life. In the evening, indulge in a delicious seafood dinner at a beachfront restaurant and witness a breathtaking sunset over the horizon.", + "locationName": "Nosy Be", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "madagascar", + "ref": "relax-on-the-beaches-of-nosy-be", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_relax-on-the-beaches-of-nosy-be.jpg" + }, + { + "name": "Embark on a Whale Watching Expedition", + "description": "Embark on an unforgettable whale watching excursion off the coast of Île Sainte-Marie during the humpback whale migration season (July to September). Witness these majestic creatures breaching and tail-slapping as they make their annual journey to warmer waters. Learn about their fascinating behaviors and the importance of marine conservation.", + "locationName": "Île Sainte-Marie", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "madagascar", + "ref": "embark-on-a-whale-watching-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_embark-on-a-whale-watching-expedition.jpg" + }, + { + "name": "Immerse Yourself in the Culture of Antananarivo", + "description": "Explore the vibrant capital city of Antananarivo, a melting pot of Malagasy culture and history. Visit the Rova of Antananarivo, a historic royal palace complex offering panoramic views of the city. Wander through the bustling Analakely Market, a sensory feast of local crafts, spices, and fresh produce. Discover the city's rich architectural heritage, including colonial buildings and traditional wooden houses.", + "locationName": "Antananarivo", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "madagascar", + "ref": "immerse-yourself-in-the-culture-of-antananarivo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_immerse-yourself-in-the-culture-of-antananarivo.jpg" + }, + { + "name": "Hike to the Top of Pic Boby", + "description": "Embark on a challenging but rewarding hike to the summit of Pic Boby, the highest peak in Madagascar. Trek through diverse landscapes, from lush rainforests to rocky terrain, and be rewarded with breathtaking panoramic views of the surrounding mountains and valleys. This adventure is perfect for experienced hikers seeking a thrilling experience.", + "locationName": "Andringitra National Park", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "madagascar", + "ref": "hike-to-the-top-of-pic-boby", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_hike-to-the-top-of-pic-boby.jpg" + }, + { + "name": "Go Scuba Diving or Snorkeling in the Indian Ocean", + "description": "Dive into the vibrant underwater world surrounding Madagascar. Explore coral reefs teeming with colorful fish, encounter majestic sea turtles, and perhaps even spot dolphins or whale sharks. Whether you're an experienced diver or a beginner snorkeler, Madagascar's waters offer an unforgettable marine experience.", + "locationName": "Nosy Be, Ile Sainte Marie", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "madagascar", + "ref": "go-scuba-diving-or-snorkeling-in-the-indian-ocean", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_go-scuba-diving-or-snorkeling-in-the-indian-ocean.jpg" + }, + { + "name": "Visit the Tsingy de Bemaraha National Park", + "description": "Explore the unique and otherworldly landscapes of Tsingy de Bemaraha National Park, a UNESCO World Heritage Site. Marvel at the towering limestone formations, known as 'tsingy,' which create a labyrinth of sharp pinnacles and canyons. Hike through this geological wonder and discover the diverse flora and fauna that call it home.", + "locationName": "Tsingy de Bemaraha National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "madagascar", + "ref": "visit-the-tsingy-de-bemaraha-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_visit-the-tsingy-de-bemaraha-national-park.jpg" + }, + { + "name": "Take a Cultural Tour of Antsirabe", + "description": "Step back in time and experience the colonial charm of Antsirabe, known as the 'Vichy of Madagascar.' Explore the city's thermal springs, visit the local market, and admire the French architecture. Take a pousse-pousse ride through the streets and soak up the relaxed atmosphere of this historic town.", + "locationName": "Antsirabe", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "madagascar", + "ref": "take-a-cultural-tour-of-antsirabe", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_take-a-cultural-tour-of-antsirabe.jpg" + }, + { + "name": "Cruise Down the Tsiribihina River", + "description": "Embark on a relaxing river journey down the Tsiribihina River, experiencing the heart of Madagascar's wilderness. Drift past lush landscapes, spot diverse wildlife along the riverbanks, and witness stunning sunsets over the water. This tranquil adventure allows you to connect with nature and observe rural life in Madagascar.", + "locationName": "Tsiribihina River", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "madagascar", + "ref": "cruise-down-the-tsiribihina-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_cruise-down-the-tsiribihina-river.jpg" + }, + { + "name": "Visit the Avenue of the Baobabs", + "description": "Witness the iconic Avenue of the Baobabs, a stunning natural corridor lined with majestic baobab trees. Capture breathtaking photos of these giants at sunset or sunrise, and learn about their significance in Malagasy culture. This is a must-see for any visitor to Madagascar.", + "locationName": "Morondava", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "madagascar", + "ref": "visit-the-avenue-of-the-baobabs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_visit-the-avenue-of-the-baobabs.jpg" + }, + { + "name": "Explore the Masoala National Park", + "description": "Embark on a jungle adventure in Masoala National Park, a haven of biodiversity. Hike through lush rainforests, spot rare lemurs, chameleons, and birds, and discover hidden waterfalls. This park offers a truly immersive experience in Madagascar's unique ecosystem.", + "locationName": "Masoala Peninsula", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "madagascar", + "ref": "explore-the-masoala-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_explore-the-masoala-national-park.jpg" + }, + { + "name": "Learn about Lemur Conservation at a Sanctuary", + "description": "Visit a lemur sanctuary and learn about the conservation efforts to protect these endangered primates. Observe various lemur species up close, learn about their behaviors and habitats, and support the crucial work of these sanctuaries.", + "locationName": "Multiple locations across Madagascar", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "madagascar", + "ref": "learn-about-lemur-conservation-at-a-sanctuary", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_learn-about-lemur-conservation-at-a-sanctuary.jpg" + }, + { + "name": "Experience Local Life in a Rural Village", + "description": "Immerse yourself in the authentic culture of Madagascar by visiting a rural village. Interact with locals, learn about their traditions and way of life, and gain a deeper understanding of the island's rich cultural heritage.", + "locationName": "Various rural villages throughout Madagascar", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "madagascar", + "ref": "experience-local-life-in-a-rural-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_experience-local-life-in-a-rural-village.jpg" + }, + { + "name": "Shop for Unique Crafts and Souvenirs", + "description": "Discover the vibrant arts and crafts scene in Madagascar. Explore local markets and shops for unique souvenirs, including handwoven textiles, wood carvings, and semi-precious stones. Find the perfect treasures to remember your trip by.", + "locationName": "Antananarivo, Antsirabe, and other major towns", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "madagascar", + "ref": "shop-for-unique-crafts-and-souvenirs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_shop-for-unique-crafts-and-souvenirs.jpg" + }, + { + "name": "Kite Surfing in Emerald Waters", + "description": "Harness the power of the wind and glide across the crystal-clear waters of Madagascar's lagoons. Whether you're a beginner or a seasoned pro, numerous spots offer ideal conditions for kite surfing. Feel the adrenaline rush as you soar through the air, surrounded by stunning coastal landscapes. Sakalava Bay, Diego Suarez, and Fort Dauphin are renowned for their consistent winds and vibrant kite surfing communities. This activity is best enjoyed during the windy season, typically between April and October.", + "locationName": "Sakalava Bay, Diego Suarez, or Fort Dauphin", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "madagascar", + "ref": "kite-surfing-in-emerald-waters", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_kite-surfing-in-emerald-waters.jpg" + }, + { + "name": "Indulge in a Malagasy Cooking Class", + "description": "Embark on a culinary journey and learn the secrets of Malagasy cuisine. Join a cooking class led by local chefs who will guide you through traditional recipes and techniques. Discover the unique blend of flavors influenced by African, Asian, and European cultures. Master the art of preparing dishes like ravitoto (cassava leaves with pork) or romazava (a flavorful meat and vegetable stew). Savor the fruits of your labor as you enjoy a delicious meal with newfound friends. This immersive experience offers a deeper understanding of Malagasy culture and its culinary traditions.", + "locationName": "Antananarivo or Nosy Be", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "madagascar", + "ref": "indulge-in-a-malagasy-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_indulge-in-a-malagasy-cooking-class.jpg" + }, + { + "name": "Go Caving in the Ankarana National Park", + "description": "Venture into the depths of the Ankarana National Park, known for its dramatic limestone formations and extensive cave systems. Explore the otherworldly underground realm, adorned with stalactites, stalagmites, and unique geological features. Discover hidden chambers, underground rivers, and diverse bat species. Guided tours offer insights into the geological history and cultural significance of these caves. This adventure is perfect for those seeking an off-the-beaten-path experience and a glimpse into Madagascar's hidden wonders.", + "locationName": "Ankarana National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "madagascar", + "ref": "go-caving-in-the-ankarana-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_go-caving-in-the-ankarana-national-park.jpg" + }, + { + "name": "Take a Scenic Train Ride on the Fianarantsoa-Côte Est Railway", + "description": "Embark on a nostalgic journey through the heart of Madagascar aboard the Fianarantsoa-Côte Est Railway. This historic railway line winds through breathtaking landscapes, offering panoramic views of lush rainforests, terraced rice fields, and charming villages. Experience the rhythm of local life as you interact with fellow passengers and witness the beauty of the countryside unfold. The train journey provides a unique perspective on Madagascar's diverse topography and cultural heritage.", + "locationName": "Fianarantsoa to Manakara", + "duration": 12, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "madagascar", + "ref": "take-a-scenic-train-ride-on-the-fianarantsoa-c-te-est-railway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_take-a-scenic-train-ride-on-the-fianarantsoa-c-te-est-railway.jpg" + }, + { + "name": "Stargaze in the Remote Wilderness", + "description": "Escape the city lights and immerse yourself in the celestial wonders of Madagascar's night sky. Head to remote areas away from light pollution, such as the Isalo National Park or the Tsingy de Bemaraha National Park, where the Milky Way and countless stars shine brightly. Join a guided stargazing tour or simply lie back and marvel at the constellations. This experience offers a sense of tranquility and a deeper connection with the natural world. Capture stunning astrophotography or simply enjoy the awe-inspiring beauty of the cosmos.", + "locationName": "Isalo National Park or Tsingy de Bemaraha National Park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "madagascar", + "ref": "stargaze-in-the-remote-wilderness", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/madagascar_stargaze-in-the-remote-wilderness.jpg" + }, + { + "name": "Scuba Diving in the North Malé Atoll", + "description": "Embark on an underwater adventure exploring the vibrant coral reefs of the North Malé Atoll. Encounter diverse marine life, including colorful fish, graceful manta rays, and majestic sharks. Dive sites like Manta Point and Banana Reef offer unforgettable experiences for both beginner and experienced divers.", + "locationName": "North Malé Atoll", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "maldives", + "ref": "scuba-diving-in-the-north-mal-atoll", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_scuba-diving-in-the-north-mal-atoll.jpg" + }, + { + "name": "Island Hopping Tour", + "description": "Discover the beauty and diversity of the Maldivian islands with an island hopping tour. Visit local islands, experience the Maldivian culture, and enjoy swimming, snorkeling, or simply relaxing on pristine beaches.", + "locationName": "Various islands", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "maldives", + "ref": "island-hopping-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_island-hopping-tour.jpg" + }, + { + "name": "Sunset Cruise with Dolphin Watching", + "description": "Sail across the turquoise waters as the sun sets, painting the sky with vibrant colors. Keep an eye out for playful dolphins as they frolic alongside the boat. Enjoy the breathtaking views and create unforgettable memories on this romantic and relaxing cruise.", + "locationName": "Departure from various resorts", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "maldives", + "ref": "sunset-cruise-with-dolphin-watching", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_sunset-cruise-with-dolphin-watching.jpg" + }, + { + "name": "Underwater Dining Experience", + "description": "Indulge in a unique and unforgettable dining experience at an underwater restaurant. Surrounded by marine life, savor delicious cuisine while marveling at the beauty of the coral reef. This is a perfect option for a special occasion or a romantic evening.", + "locationName": "Conrad Maldives Rangali Island", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "maldives", + "ref": "underwater-dining-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_underwater-dining-experience.jpg" + }, + { + "name": "Relaxation at a Spa Resort", + "description": "Escape to a luxurious spa resort and indulge in rejuvenating treatments. Enjoy massages, facials, and body wraps inspired by traditional Maldivian techniques and natural ingredients. Unwind in a serene atmosphere and let your worries melt away.", + "locationName": "Various spa resorts", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "maldives", + "ref": "relaxation-at-a-spa-resort", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_relaxation-at-a-spa-resort.jpg" + }, + { + "name": "Surfing in the North Malé Atoll", + "description": "Experience the thrill of riding the waves in the turquoise waters of the North Malé Atoll. Several surf breaks cater to different skill levels, from beginners to experienced surfers. Rent a board and catch some waves, or take a lesson from a local instructor to improve your technique.", + "locationName": "North Malé Atoll", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "maldives", + "ref": "surfing-in-the-north-mal-atoll", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_surfing-in-the-north-mal-atoll.jpg" + }, + { + "name": "Local Island Experience", + "description": "Immerse yourself in the Maldivian culture by visiting a local island. Explore the traditional way of life, interact with friendly locals, and discover hidden gems away from the tourist resorts. Visit local markets, try authentic Maldivian cuisine, and learn about the island's history and traditions.", + "locationName": "Maafushi, Fulidhoo, or Thoddoo", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "maldives", + "ref": "local-island-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_local-island-experience.jpg" + }, + { + "name": "Snorkeling with Manta Rays", + "description": "Embark on an unforgettable snorkeling adventure to encounter the majestic manta rays. Witness these gentle giants glide gracefully through the water as you swim alongside them. Several tour operators offer snorkeling trips to manta ray cleaning stations, where you can observe their feeding habits.", + "locationName": "Hanifaru Bay or Addu Atoll", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "maldives", + "ref": "snorkeling-with-manta-rays", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_snorkeling-with-manta-rays.jpg" + }, + { + "name": "Sunset Fishing Trip", + "description": "Experience the Maldivian tradition of handline fishing on a sunset cruise. Learn about local fishing techniques, try your luck at catching some fish, and enjoy the stunning views of the sun setting over the Indian Ocean. Many excursions offer the option to grill your catch for a fresh and delicious dinner.", + "locationName": "Various locations throughout the Maldives", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "maldives", + "ref": "sunset-fishing-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_sunset-fishing-trip.jpg" + }, + { + "name": "Bioluminescent Plankton Tour", + "description": "Witness the magical phenomenon of bioluminescent plankton on a night tour. As you move through the water, the plankton illuminates, creating a mesmerizing spectacle of sparkling blue light. This unique experience is a must-do for nature enthusiasts and photographers.", + "locationName": "Vaadhoo Island or Mudhdhoo Island", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "maldives", + "ref": "bioluminescent-plankton-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_bioluminescent-plankton-tour.jpg" + }, + { + "name": "Kayaking in the Turquoise Lagoons", + "description": "Embark on a serene kayaking adventure through the crystal-clear turquoise lagoons of the Maldives. Paddle at your own pace, exploring hidden coves, mangrove forests, and small, deserted islands. Witness the vibrant marine life below and soak in the tranquility of the surrounding nature. This activity is perfect for a relaxing and eco-friendly exploration of the Maldivian waters.", + "locationName": "Various atolls and islands", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "maldives", + "ref": "kayaking-in-the-turquoise-lagoons", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_kayaking-in-the-turquoise-lagoons.jpg" + }, + { + "name": "Sandbank Picnic and Stargazing", + "description": "Escape to a secluded sandbank for a romantic and unforgettable experience. Enjoy a private picnic with delicious Maldivian cuisine as the sun sets over the horizon. As darkness falls, marvel at the breathtaking view of the starlit sky, away from any light pollution. This intimate activity is perfect for couples or anyone seeking a moment of tranquility and wonder.", + "locationName": "Sandbanks near resort islands", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "maldives", + "ref": "sandbank-picnic-and-stargazing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_sandbank-picnic-and-stargazing.jpg" + }, + { + "name": "Hydrofoil Trip to a Local Village", + "description": "Experience the authentic Maldivian culture with a hydrofoil trip to a nearby local island. Immerse yourself in the daily life of the islanders, visit traditional markets, and interact with friendly locals. Learn about their customs, traditions, and way of life. This cultural excursion provides a unique perspective beyond the resort experience.", + "locationName": "Local islands near resort areas", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "maldives", + "ref": "hydrofoil-trip-to-a-local-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_hydrofoil-trip-to-a-local-village.jpg" + }, + { + "name": "Windsurfing and Kitesurfing Adventures", + "description": "For thrill-seekers, the Maldives offers ideal conditions for windsurfing and kitesurfing. Harness the power of the wind and glide across the turquoise waters, surrounded by breathtaking scenery. Lessons and equipment rentals are available for all skill levels, making it a perfect activity for both beginners and experienced riders.", + "locationName": "Various atolls and islands with water sports centers", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "maldives", + "ref": "windsurfing-and-kitesurfing-adventures", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_windsurfing-and-kitesurfing-adventures.jpg" + }, + { + "name": "Maldivian Cooking Class", + "description": "Delve into the world of Maldivian cuisine with a hands-on cooking class. Learn to prepare traditional dishes using fresh, local ingredients, and discover the unique flavors and spices that characterize Maldivian food. This immersive experience allows you to take a piece of Maldivian culture home with you.", + "locationName": "Resorts or local islands offering cooking classes", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "maldives", + "ref": "maldivian-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_maldivian-cooking-class.jpg" + }, + { + "name": "Submarine Dive", + "description": "Embark on an unforgettable underwater adventure aboard a submarine, descending into the depths of the Indian Ocean to witness the vibrant marine life and coral reefs without getting wet. Marvel at colorful fish, graceful manta rays, and even elusive sharks as you explore the hidden wonders of the Maldivian waters.", + "locationName": "Malé Atoll", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "maldives", + "ref": "submarine-dive", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_submarine-dive.jpg" + }, + { + "name": "Whale Shark Excursion", + "description": "Join a thrilling excursion to swim alongside gentle giants – whale sharks. These majestic creatures frequent the Maldivian waters, offering a once-in-a-lifetime opportunity to witness their awe-inspiring size and graceful movements. Experienced guides will ensure a safe and unforgettable encounter.", + "locationName": "South Ari Atoll", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "maldives", + "ref": "whale-shark-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_whale-shark-excursion.jpg" + }, + { + "name": "Seaplane Photo Flight", + "description": "Take to the skies for a breathtaking seaplane photo flight, capturing the stunning aerial views of the Maldives' turquoise lagoons, white-sand beaches, and lush islands. This experience provides unparalleled perspectives and the perfect opportunity to create lasting memories through photography.", + "locationName": "Various Atolls", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "maldives", + "ref": "seaplane-photo-flight", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_seaplane-photo-flight.jpg" + }, + { + "name": "Local Market Visit and Cooking Class", + "description": "Immerse yourself in the local culture with a visit to a vibrant Maldivian market. Explore the sights and smells of fresh produce, spices, and local crafts. Then, participate in a hands-on cooking class to learn the secrets of Maldivian cuisine, creating delicious dishes to savor.", + "locationName": "Malé or Maafushi", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "maldives", + "ref": "local-market-visit-and-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maldives_local-market-visit-and-cooking-class.jpg" + }, + { + "name": "Explore the Historic City of Palma", + "description": "Wander through the charming streets of Palma, the capital city of Mallorca. Visit the iconic Palma Cathedral, a stunning Gothic masterpiece, and explore the Royal Palace of La Almudaina. Discover the Arab Baths, a historic hammam dating back to the Moorish era, and lose yourself in the labyrinthine alleyways of the Old Town. Enjoy tapas and local wines at a sidewalk cafe, and soak up the vibrant atmosphere of this historic city.", + "locationName": "Palma", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "mallorca", + "ref": "explore-the-historic-city-of-palma", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_explore-the-historic-city-of-palma.jpg" + }, + { + "name": "Hike the Serra de Tramuntana", + "description": "Embark on a scenic hike through the Serra de Tramuntana mountain range, a UNESCO World Heritage Site. Choose from various trails, ranging from easy to challenging, and enjoy breathtaking views of the coastline, hidden coves, and charming villages. Discover ancient monasteries, such as Lluc Monastery, and experience the tranquility of the mountains.", + "locationName": "Serra de Tramuntana", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "mallorca", + "ref": "hike-the-serra-de-tramuntana", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_hike-the-serra-de-tramuntana.jpg" + }, + { + "name": "Relax on Beautiful Beaches", + "description": "Unwind on the pristine beaches of Mallorca, known for their turquoise waters and soft sand. Choose from popular beaches like Cala Millor and Playa de Palma, or discover secluded coves like Cala Mesquida and Cala Agulla. Enjoy swimming, sunbathing, and water sports, or simply relax and enjoy the Mediterranean sunshine.", + "locationName": "Various Beaches", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "mallorca", + "ref": "relax-on-beautiful-beaches", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_relax-on-beautiful-beaches.jpg" + }, + { + "name": "Discover the Caves of Drach", + "description": "Explore the fascinating Caves of Drach, a series of four caves known for their impressive stalactites, stalagmites, and underground lake. Take a boat ride across Lake Martel, one of the largest underground lakes in the world, and enjoy a classical music concert inside the cave.", + "locationName": "Caves of Drach", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mallorca", + "ref": "discover-the-caves-of-drach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_discover-the-caves-of-drach.jpg" + }, + { + "name": "Indulge in Mallorcan Cuisine", + "description": "Experience the delicious flavors of Mallorcan cuisine. Sample local specialties such as sobrasada (cured sausage), tumbet (vegetable stew), and ensaimada (spiral pastry). Visit traditional markets and local restaurants to savor the authentic tastes of the island. Don't forget to try the local wines, such as Binissalem and Pla i Llevant.", + "locationName": "Various Restaurants and Markets", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "mallorca", + "ref": "indulge-in-mallorcan-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_indulge-in-mallorcan-cuisine.jpg" + }, + { + "name": "Soar Above the Island in a Hot Air Balloon", + "description": "Experience the breathtaking beauty of Mallorca from a unique perspective with a hot air balloon ride. As you gently ascend, marvel at panoramic views of the island's diverse landscapes, from the azure waters of the Mediterranean to the rolling hills and charming villages. This serene and unforgettable experience is perfect for a romantic getaway or a special occasion.", + "locationName": "Various locations across the island", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "mallorca", + "ref": "soar-above-the-island-in-a-hot-air-balloon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_soar-above-the-island-in-a-hot-air-balloon.jpg" + }, + { + "name": "Embark on a Catamaran Cruise to Secluded Coves", + "description": "Set sail on a catamaran adventure to discover hidden coves and pristine beaches inaccessible by land. Dive into crystal-clear waters for a refreshing swim, snorkel among vibrant marine life, or simply relax on deck and soak up the Mediterranean sun. Many cruises include delicious meals and drinks, making for a perfect day trip.", + "locationName": "Departs from various ports", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mallorca", + "ref": "embark-on-a-catamaran-cruise-to-secluded-coves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_embark-on-a-catamaran-cruise-to-secluded-coves.jpg" + }, + { + "name": "Visit the Picturesque Villages of Valldemossa and Deià", + "description": "Escape the bustling city and explore the charming villages nestled in the Tramuntana mountains. Valldemossa, with its historic monastery and cobbled streets, offers a glimpse into Mallorca's rich past. Deià, a haven for artists and writers, boasts stunning coastal views and a bohemian atmosphere. Enjoy local crafts, art galleries, and delicious cuisine.", + "locationName": "Valldemossa and Deià", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "mallorca", + "ref": "visit-the-picturesque-villages-of-valldemossa-and-dei-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_visit-the-picturesque-villages-of-valldemossa-and-dei-.jpg" + }, + { + "name": "Go Kayaking or Stand-Up Paddleboarding along the Coast", + "description": "Explore Mallorca's coastline at your own pace with a kayaking or stand-up paddleboarding excursion. Paddle through turquoise waters, discover hidden caves and secluded beaches, and admire the dramatic cliffs. This active adventure is perfect for nature enthusiasts and water sports lovers.", + "locationName": "Various beaches and coves", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "mallorca", + "ref": "go-kayaking-or-stand-up-paddleboarding-along-the-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_go-kayaking-or-stand-up-paddleboarding-along-the-coast.jpg" + }, + { + "name": "Experience the Thrill of Caving in the Coves del Hams", + "description": "Embark on an underground adventure through the Coves del Hams, a network of stunning caves adorned with stalactites, stalagmites, and an underground lake. Take a guided tour to learn about the geological formations and enjoy a magical musical boat ride on the lake. This unique experience is perfect for families and adventure seekers.", + "locationName": "Porto Cristo", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mallorca", + "ref": "experience-the-thrill-of-caving-in-the-coves-del-hams", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_experience-the-thrill-of-caving-in-the-coves-del-hams.jpg" + }, + { + "name": "Cycling Through Scenic Landscapes", + "description": "Embark on a cycling adventure through Mallorca's diverse landscapes. Pedal along coastal roads with breathtaking sea views, meander through charming villages, or challenge yourself with mountain climbs in the Serra de Tramuntana. With various routes for all levels, cycling offers a unique way to explore the island's beauty.", + "locationName": "Various routes throughout Mallorca", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "mallorca", + "ref": "cycling-through-scenic-landscapes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_cycling-through-scenic-landscapes.jpg" + }, + { + "name": "Wine Tasting in the Binissalem Region", + "description": "Delve into the world of Mallorcan wines with a visit to the Binissalem region. Explore family-run vineyards, learn about the island's winemaking traditions, and indulge in tastings of local varieties like Manto Negro and Callet. Enjoy the scenic beauty of the vineyards and savor the flavors of Mallorca.", + "locationName": "Binissalem region", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "mallorca", + "ref": "wine-tasting-in-the-binissalem-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_wine-tasting-in-the-binissalem-region.jpg" + }, + { + "name": "Stargazing in the Tramuntana Mountains", + "description": "Escape the city lights and experience the magic of stargazing in the Tramuntana Mountains. Away from light pollution, the clear night sky offers a breathtaking view of constellations and shooting stars. Join a guided tour or find a secluded spot to marvel at the celestial wonders above.", + "locationName": "Serra de Tramuntana", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "mallorca", + "ref": "stargazing-in-the-tramuntana-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_stargazing-in-the-tramuntana-mountains.jpg" + }, + { + "name": "Horseback Riding through Rural Landscapes", + "description": "Explore the heart of Mallorca on horseback, traversing rural paths and picturesque landscapes. Whether you're a seasoned rider or a beginner, there are options for all levels. Enjoy the tranquility of nature, discover hidden trails, and experience the island from a unique perspective.", + "locationName": "Rural areas of Mallorca", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mallorca", + "ref": "horseback-riding-through-rural-landscapes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_horseback-riding-through-rural-landscapes.jpg" + }, + { + "name": "Explore the Palma Aquarium", + "description": "Dive into the fascinating underwater world at Palma Aquarium. Discover a diverse array of marine life, from colorful fish and majestic sharks to playful sea turtles and graceful jellyfish. Learn about marine conservation efforts and enjoy interactive exhibits that are both educational and entertaining for all ages.", + "locationName": "Palma", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "mallorca", + "ref": "explore-the-palma-aquarium", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_explore-the-palma-aquarium.jpg" + }, + { + "name": "Scuba Diving in Marine Reserves", + "description": "Explore the underwater wonders of Mallorca's marine reserves, such as El Toro or Cabrera Archipelago. Discover vibrant coral reefs, diverse marine life, and even underwater caves, making for an unforgettable diving experience.", + "locationName": "El Toro Marine Reserve or Cabrera Archipelago", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "mallorca", + "ref": "scuba-diving-in-marine-reserves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_scuba-diving-in-marine-reserves.jpg" + }, + { + "name": "Sunset Horseback Riding", + "description": "Embark on a magical horseback riding adventure as the sun sets over the Mallorcan countryside. Enjoy breathtaking views of rolling hills, vineyards, and the distant Mediterranean Sea, creating a romantic and unforgettable experience.", + "locationName": "Rural Mallorca", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "mallorca", + "ref": "sunset-horseback-riding", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_sunset-horseback-riding.jpg" + }, + { + "name": "Take a Cooking Class and Learn to Make Paella", + "description": "Immerse yourself in Mallorcan culture by taking a cooking class and learning how to prepare the island's iconic dish, paella. Master the art of creating the perfect blend of rice, seafood, and spices, and savor the delicious results of your culinary efforts.", + "locationName": "Palma or other towns", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "mallorca", + "ref": "take-a-cooking-class-and-learn-to-make-paella", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_take-a-cooking-class-and-learn-to-make-paella.jpg" + }, + { + "name": "Go Cliff Jumping at Cala S'Almunia", + "description": "For the adventurous souls, head to Cala S'Almunia, a secluded cove known for its dramatic cliffs and crystal-clear waters. Take the plunge and experience the thrill of cliff jumping into the refreshing Mediterranean Sea.", + "locationName": "Cala S'Almunia", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "mallorca", + "ref": "go-cliff-jumping-at-cala-s-almunia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_go-cliff-jumping-at-cala-s-almunia.jpg" + }, + { + "name": "Enjoy Live Music at a Jazz Club", + "description": "Experience the vibrant nightlife of Mallorca by catching live music at a jazz club in Palma or other towns. Enjoy the soulful sounds of local and international musicians while sipping on cocktails and soaking up the atmosphere.", + "locationName": "Palma or other towns", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "mallorca", + "ref": "enjoy-live-music-at-a-jazz-club", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mallorca_enjoy-live-music-at-a-jazz-club.jpg" + }, + { + "name": "Explore the Historic City of Valletta", + "description": "Step back in time as you wander through the charming streets of Valletta, a UNESCO World Heritage Site. Admire the Baroque architecture, visit St. John's Co-Cathedral with its opulent interior, and enjoy panoramic views of the Grand Harbour from the Upper Barrakka Gardens. History buffs will be enthralled by the rich past of this fortified city.", + "locationName": "Valletta", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "malta", + "ref": "explore-the-historic-city-of-valletta", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_explore-the-historic-city-of-valletta.jpg" + }, + { + "name": "Dive into the Blue Lagoon", + "description": "Escape to the island of Comino and discover the breathtaking Blue Lagoon. Dive into the crystal-clear turquoise waters, perfect for swimming, snorkeling, and scuba diving. Relax on the white sandy beach or explore the nearby caves and coves. This is a must-do for water enthusiasts and those seeking a slice of paradise.", + "locationName": "Comino", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "malta", + "ref": "dive-into-the-blue-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_dive-into-the-blue-lagoon.jpg" + }, + { + "name": "Discover the Ancient Megalithic Temples", + "description": "Embark on a journey through Malta's prehistoric past by visiting the Megalithic Temples, some of the oldest freestanding structures in the world. Explore the UNESCO-listed Ħaġar Qim and Mnajdra temples, marveling at their intricate construction and mysterious origins. This is a unique experience for history and archaeology enthusiasts.", + "locationName": "Ħaġar Qim and Mnajdra", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "malta", + "ref": "discover-the-ancient-megalithic-temples", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_discover-the-ancient-megalithic-temples.jpg" + }, + { + "name": "Indulge in Maltese Cuisine", + "description": "Tantalize your taste buds with the delicious flavors of Maltese cuisine. Sample traditional dishes like pastizzi (savory pastries), rabbit stew, and lampuki pie (fish pie). Explore the vibrant food markets and dine at charming local restaurants. Food tours are a great way to experience the culinary delights of Malta.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "malta", + "ref": "indulge-in-maltese-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_indulge-in-maltese-cuisine.jpg" + }, + { + "name": "Experience the Nightlife in Paceville", + "description": "When the sun goes down, head to Paceville, Malta's vibrant nightlife hub. Dance the night away at clubs, enjoy live music at bars, and try your luck at the casinos. With a diverse range of venues and a lively atmosphere, Paceville offers something for everyone looking for evening entertainment.", + "locationName": "Paceville", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "malta", + "ref": "experience-the-nightlife-in-paceville", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_experience-the-nightlife-in-paceville.jpg" + }, + { + "name": "Gozo Island Adventure", + "description": "Embark on a day trip to Gozo, Malta's sister island, renowned for its tranquil countryside and dramatic coastline. Explore the charming Citadel in Victoria, visit the stunning Azure Window (collapsed natural arch), and relax on the red sands of Ramla Bay. Hike or bike through the picturesque landscapes, discovering hidden coves and historical sites. Gozo offers a slower pace of life and a taste of authentic Maltese culture.", + "locationName": "Gozo Island", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "malta", + "ref": "gozo-island-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_gozo-island-adventure.jpg" + }, + { + "name": "Sailing & Snorkeling in Comino's Blue Lagoon", + "description": "Set sail on a boat trip to Comino, a small island between Malta and Gozo, famed for its Blue Lagoon. Immerse yourself in the crystal-clear turquoise waters, ideal for swimming, snorkeling, and diving. Discover the vibrant marine life and underwater caves, or simply soak up the sun on the pristine white sand beaches. Enjoy a leisurely lunch on board and breathtaking views of the Mediterranean coastline.", + "locationName": "Comino", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "malta", + "ref": "sailing-snorkeling-in-comino-s-blue-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_sailing-snorkeling-in-comino-s-blue-lagoon.jpg" + }, + { + "name": "Marsaxlokk Fishing Village & Market", + "description": "Experience the charm of Marsaxlokk, a traditional fishing village with a vibrant Sunday market. Stroll along the waterfront, admiring the colorful luzzu boats and enjoying the fresh sea breeze. Browse the stalls overflowing with local produce, handicrafts, and souvenirs. Savor a delicious seafood lunch at one of the many waterfront restaurants, indulging in the authentic flavors of Malta.", + "locationName": "Marsaxlokk", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "malta", + "ref": "marsaxlokk-fishing-village-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_marsaxlokk-fishing-village-market.jpg" + }, + { + "name": "Mdina: The Silent City", + "description": "Step back in time in Mdina, Malta's ancient capital, known as the \"Silent City.\" Wander through its narrow, winding streets, admiring the medieval and baroque architecture. Explore the imposing bastions, offering panoramic views of the island. Visit St. Paul's Cathedral and the Mdina Dungeons, delving into the city's rich history. Enjoy a peaceful escape from the bustling modern world.", + "locationName": "Mdina", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "malta", + "ref": "mdina-the-silent-city", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_mdina-the-silent-city.jpg" + }, + { + "name": "Hiking & History on Dingli Cliffs", + "description": "Embark on a scenic hike along the Dingli Cliffs, the highest point in Malta, offering breathtaking views of the Mediterranean Sea. Explore the rugged coastline, discovering hidden caves and historical sites, including the Clapham Junction cart ruts and the prehistoric Misqa Tanks. Immerse yourself in the natural beauty and learn about Malta's geological and cultural heritage. This adventurous experience is perfect for nature lovers and history enthusiasts.", + "locationName": "Dingli Cliffs", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "malta", + "ref": "hiking-history-on-dingli-cliffs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_hiking-history-on-dingli-cliffs.jpg" + }, + { + "name": "Sunset Kayak Tour of Golden Bay", + "description": "Embark on a magical kayaking adventure as the sun dips below the horizon, casting a golden glow on the picturesque Golden Bay. Paddle through the tranquil waters, exploring hidden caves and dramatic cliffs while enjoying the breathtaking views of the Maltese coastline. This serene experience is perfect for nature lovers and those seeking a romantic evening activity.", + "locationName": "Golden Bay", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "malta", + "ref": "sunset-kayak-tour-of-golden-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_sunset-kayak-tour-of-golden-bay.jpg" + }, + { + "name": "Wine Tasting in the Maltese Countryside", + "description": "Indulge in the flavors of Malta with a delightful wine tasting experience at a local vineyard. Discover the unique characteristics of Maltese wines, from crisp whites to robust reds, while learning about the island's rich viticulture history. Enjoy the scenic views of rolling vineyards and charming villages as you savor the tastes of Malta.", + "locationName": "Maltese Countryside", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "malta", + "ref": "wine-tasting-in-the-maltese-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_wine-tasting-in-the-maltese-countryside.jpg" + }, + { + "name": "Horseback Riding through Gozo's Countryside", + "description": "Explore the idyllic countryside of Gozo on horseback, trotting along scenic trails and enjoying the peaceful atmosphere. This activity is suitable for all experience levels, allowing you to connect with nature and discover hidden corners of the island at your own pace. The stunning landscapes and charming villages will create unforgettable memories.", + "locationName": "Gozo", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "malta", + "ref": "horseback-riding-through-gozo-s-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_horseback-riding-through-gozo-s-countryside.jpg" + }, + { + "name": "Rock Climbing Adventure in Wied il-Għasri", + "description": "Challenge yourself with an exhilarating rock climbing experience in Wied il-Għasri, a breathtaking valley with towering cliffs. With routes for various skill levels, both beginners and experienced climbers can enjoy the thrill of conquering the rock face while taking in the stunning views of the Mediterranean Sea. This adventurous activity is perfect for adrenaline seekers.", + "locationName": "Wied il-Għasri", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "malta", + "ref": "rock-climbing-adventure-in-wied-il-g-asri", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_rock-climbing-adventure-in-wied-il-g-asri.jpg" + }, + { + "name": "Spa Day at a Luxury Resort", + "description": "Unwind and rejuvenate with a pampering spa day at one of Malta's luxurious resorts. Indulge in a variety of treatments, including massages, facials, and body wraps, designed to relax your mind and body. Enjoy the serene ambiance and breathtaking views as you escape the stresses of everyday life.", + "locationName": "Various Luxury Resorts", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "malta", + "ref": "spa-day-at-a-luxury-resort", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_spa-day-at-a-luxury-resort.jpg" + }, + { + "name": "Birdwatching at Għadira Nature Reserve", + "description": "Embark on a tranquil birdwatching experience at Għadira Nature Reserve, a haven for migratory birds. Observe various species, including flamingos, herons, and ospreys, in their natural habitat. Enjoy the serene atmosphere and learn about Malta's diverse avian population.", + "locationName": "Għadira Nature Reserve", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "malta", + "ref": "birdwatching-at-g-adira-nature-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_birdwatching-at-g-adira-nature-reserve.jpg" + }, + { + "name": "Explore the Three Cities", + "description": "Step back in time and explore the charming Three Cities: Vittoriosa, Senglea, and Cospicua. Wander through narrow streets, admire historical architecture, and visit the Inquisitor's Palace. Enjoy breathtaking views of the Grand Harbour and immerse yourself in Malta's rich maritime history.", + "locationName": "The Three Cities (Vittoriosa, Senglea, Cospicua)", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "malta", + "ref": "explore-the-three-cities", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_explore-the-three-cities.jpg" + }, + { + "name": "Stargazing on Comino Island", + "description": "Escape the city lights and experience the magic of stargazing on Comino Island. With minimal light pollution, the night sky comes alive with a breathtaking display of stars. Relax on the beach, identify constellations, and marvel at the wonders of the universe.", + "locationName": "Comino Island", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "malta", + "ref": "stargazing-on-comino-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_stargazing-on-comino-island.jpg" + }, + { + "name": "Visit the Malta National Aquarium", + "description": "Embark on an underwater adventure at the Malta National Aquarium. Discover a diverse range of marine life from the Mediterranean Sea and beyond. Explore themed zones, walk through a tunnel surrounded by sharks, and learn about the importance of ocean conservation.", + "locationName": "Malta National Aquarium", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "malta", + "ref": "visit-the-malta-national-aquarium", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_visit-the-malta-national-aquarium.jpg" + }, + { + "name": "Traditional Maltese Cooking Class", + "description": "Immerse yourself in Maltese culture with a hands-on cooking class. Learn to prepare traditional dishes like rabbit stew, pastizzi, and lampuki pie. Discover the secrets of Maltese cuisine and enjoy the fruits of your labor with a delicious meal.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "malta", + "ref": "traditional-maltese-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/malta_traditional-maltese-cooking-class.jpg" + }, + { + "name": "Explore the Jemaa el-Fna Square", + "description": "Immerse yourself in the vibrant heart of Marrakech at the Jemaa el-Fna Square. Witness the captivating spectacle of snake charmers, storytellers, musicians, and food vendors. As the day transitions into evening, the square transforms with the aromas of local delicacies and the mesmerizing energy of street performers. This cultural experience is a must for any visitor to Marrakech.", + "locationName": "Jemaa el-Fna Square", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "marrakech", + "ref": "explore-the-jemaa-el-fna-square", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_explore-the-jemaa-el-fna-square.jpg" + }, + { + "name": "Wander through the Souks", + "description": "Embark on a sensory adventure through the labyrinthine alleyways of the Marrakech souks. Discover a treasure trove of handcrafted goods, from intricately woven carpets and vibrant ceramics to aromatic spices and dazzling jewelry. Practice your bargaining skills and find unique souvenirs to commemorate your trip.", + "locationName": "Marrakech Souks", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "marrakech", + "ref": "wander-through-the-souks", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_wander-through-the-souks.jpg" + }, + { + "name": "Visit the Majorelle Garden", + "description": "Escape the city bustle and find serenity in the enchanting Majorelle Garden. Stroll through lush pathways adorned with vibrant blue buildings, exotic plants, and tranquil pools. This botanical oasis offers a peaceful retreat and a perfect opportunity for photography enthusiasts.", + "locationName": "Majorelle Garden", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "marrakech", + "ref": "visit-the-majorelle-garden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_visit-the-majorelle-garden.jpg" + }, + { + "name": "Indulge in a Traditional Hammam Experience", + "description": "Treat yourself to a rejuvenating hammam experience, a quintessential part of Moroccan culture. Relax and unwind in a steam bath, followed by a thorough exfoliation and massage. Emerge feeling refreshed and revitalized, ready to continue your Marrakech adventure.", + "locationName": "Various Hammams throughout the city", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "marrakech", + "ref": "indulge-in-a-traditional-hammam-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_indulge-in-a-traditional-hammam-experience.jpg" + }, + { + "name": "Savor Moroccan Cuisine", + "description": "Embark on a culinary journey through the flavors of Morocco. Sample traditional dishes like tagine, couscous, and pastilla. Explore the Djemaa el-Fna night market for an array of street food options or enjoy a fine dining experience at a rooftop restaurant with stunning city views.", + "locationName": "Various restaurants and food stalls throughout the city", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "marrakech", + "ref": "savor-moroccan-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_savor-moroccan-cuisine.jpg" + }, + { + "name": "Hot Air Balloon Ride over the Atlas Mountains", + "description": "Embark on an unforgettable adventure with a sunrise hot air balloon ride over the majestic Atlas Mountains. Witness breathtaking panoramic views of the rugged landscape, traditional Berber villages, and the vast desert as you gently float above it all. This once-in-a-lifetime experience offers a unique perspective of Morocco's natural beauty and is perfect for capturing stunning photos.", + "locationName": "Atlas Mountains", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "marrakech", + "ref": "hot-air-balloon-ride-over-the-atlas-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_hot-air-balloon-ride-over-the-atlas-mountains.jpg" + }, + { + "name": "Day Trip to Essaouira", + "description": "Escape the bustling city life and embark on a day trip to the charming coastal town of Essaouira. Known for its laid-back atmosphere, beautiful beaches, and vibrant art scene, Essaouira offers a refreshing change of pace. Explore the historic ramparts, wander through the colourful medina, or relax on the sandy shores while enjoying the cool ocean breeze.", + "locationName": "Essaouira", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "marrakech", + "ref": "day-trip-to-essaouira", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_day-trip-to-essaouira.jpg" + }, + { + "name": "Quad Biking in the Agafay Desert", + "description": "Experience the thrill of adventure with a quad biking excursion through the Agafay Desert. Feel the adrenaline rush as you navigate the rocky terrain and sandy dunes, surrounded by stunning desert landscapes. This exciting activity offers a unique way to explore the Moroccan desert and is perfect for those seeking an adrenaline-pumping adventure.", + "locationName": "Agafay Desert", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "marrakech", + "ref": "quad-biking-in-the-agafay-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_quad-biking-in-the-agafay-desert.jpg" + }, + { + "name": "Visit the Bahia Palace", + "description": "Immerse yourself in Moroccan history and architecture with a visit to the Bahia Palace. This stunning 19th-century palace is a masterpiece of intricate design and craftsmanship, featuring beautiful courtyards, ornate rooms, and lush gardens. Explore the palace's rich history and marvel at its exquisite details, offering a glimpse into the opulent lifestyle of Moroccan royalty.", + "locationName": "Bahia Palace", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "marrakech", + "ref": "visit-the-bahia-palace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_visit-the-bahia-palace.jpg" + }, + { + "name": "Enjoy a Cooking Class", + "description": "Delve into the world of Moroccan cuisine with a hands-on cooking class. Learn the secrets of traditional dishes from experienced local chefs, using fresh, aromatic ingredients. Master the art of preparing tagines, couscous, and other culinary delights, and gain a deeper appreciation for Moroccan culture through its food.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "marrakech", + "ref": "enjoy-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_enjoy-a-cooking-class.jpg" + }, + { + "name": "Atlas Mountains Trek", + "description": "Embark on a breathtaking journey through the rugged landscapes of the Atlas Mountains. Hike through picturesque valleys, encounter Berber villages, and witness panoramic views from towering peaks. Choose from various trails suitable for different fitness levels and durations, allowing you to connect with nature and experience the serenity of the mountains.", + "locationName": "Atlas Mountains", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "marrakech", + "ref": "atlas-mountains-trek", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_atlas-mountains-trek.jpg" + }, + { + "name": "Ouzoud Waterfalls Excursion", + "description": "Escape the city bustle and visit the mesmerizing Ouzoud Waterfalls, cascading down a series of tiers amidst lush greenery. Take a refreshing dip in the natural pools, witness playful monkeys swinging through the trees, and enjoy a scenic boat ride for a closer look at the falls' beauty. This natural wonder offers a perfect retreat for nature lovers and photographers.", + "locationName": "Ouzoud", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "marrakech", + "ref": "ouzoud-waterfalls-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_ouzoud-waterfalls-excursion.jpg" + }, + { + "name": "Sunset Camel Ride in the Agafay Desert", + "description": "Experience the magic of the desert with a captivating camel ride as the sun sets over the Agafay Desert. Traverse the undulating dunes, witness the changing hues of the sky, and immerse yourself in the tranquility of the landscape. This romantic and unforgettable adventure offers a unique perspective of Morocco's natural beauty.", + "locationName": "Agafay Desert", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "marrakech", + "ref": "sunset-camel-ride-in-the-agafay-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_sunset-camel-ride-in-the-agafay-desert.jpg" + }, + { + "name": "Jardin Secret Discovery", + "description": "Step into a hidden oasis in the heart of the Medina at Le Jardin Secret. Explore the beautifully restored Islamic gardens, featuring intricate water features, lush vegetation, and serene courtyards. Discover the fascinating history of the gardens and admire the traditional Moroccan architecture, offering a peaceful escape from the city's vibrant streets.", + "locationName": "Le Jardin Secret", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "marrakech", + "ref": "jardin-secret-discovery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_jardin-secret-discovery.jpg" + }, + { + "name": "Live Music at a Traditional Riad", + "description": "Immerse yourself in the enchanting atmosphere of a traditional riad and enjoy an evening of live Moroccan music. Listen to the mesmerizing melodies of local musicians, experience the rhythmic beats of Gnawa music, or sway to the soulful tunes of Andalusian sounds. This cultural experience offers a glimpse into the rich musical heritage of Morocco.", + "locationName": "Various Riads", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "marrakech", + "ref": "live-music-at-a-traditional-riad", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_live-music-at-a-traditional-riad.jpg" + }, + { + "name": "Horse-Drawn Carriage Ride through Marrakech", + "description": "Embark on a charming horse-drawn carriage ride through the enchanting streets of Marrakech. Clip-clop your way past iconic landmarks, hidden squares, and vibrant neighbourhoods, experiencing the city's charm from a unique perspective. This leisurely tour offers a relaxing way to soak in the sights and sounds of Marrakech.", + "locationName": "Marrakech City Center", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "marrakech", + "ref": "horse-drawn-carriage-ride-through-marrakech", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_horse-drawn-carriage-ride-through-marrakech.jpg" + }, + { + "name": "Visit the Saadian Tombs", + "description": "Step back in time and discover the opulent Saadian Tombs, a historical necropolis dating back to the Saadian dynasty. Marvel at the intricate carvings, colourful mosaics, and peaceful courtyards that adorn the tombs of sultans, their families, and close advisors. This cultural experience offers a glimpse into Morocco's rich history and architectural grandeur.", + "locationName": "Saadian Tombs", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "marrakech", + "ref": "visit-the-saadian-tombs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_visit-the-saadian-tombs.jpg" + }, + { + "name": "Explore the Mellah (Jewish Quarter)", + "description": "Delve into the historical Mellah, the Jewish Quarter of Marrakech, and uncover its unique cultural heritage. Wander through its narrow streets, visit the synagogues, and explore the spice markets. Learn about the Jewish community's history and traditions in Morocco, gaining a deeper understanding of the city's diverse cultural tapestry.", + "locationName": "Mellah (Jewish Quarter)", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "marrakech", + "ref": "explore-the-mellah-jewish-quarter-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_explore-the-mellah-jewish-quarter-.jpg" + }, + { + "name": "Enjoy a Rooftop Dinner with Views", + "description": "As the sun sets over Marrakech, ascend to a rooftop restaurant and savour a delectable dinner with panoramic views of the city. Indulge in traditional Moroccan cuisine or international flavours, while enjoying the cool evening breeze and the twinkling city lights. This romantic experience offers a memorable way to end a day of exploration.", + "locationName": "Various rooftop restaurants", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "marrakech", + "ref": "enjoy-a-rooftop-dinner-with-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_enjoy-a-rooftop-dinner-with-views.jpg" + }, + { + "name": "Attend a Fantasia Show", + "description": "Immerse yourself in the vibrant spectacle of a Fantasia show, a traditional Moroccan equestrian performance. Witness skilled horsemen charging on horseback, performing daring stunts, and showcasing their horsemanship prowess. The show often includes music, acrobatics, and colourful costumes, creating a captivating cultural experience.", + "locationName": "Chez Ali or other Fantasia venues", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "marrakech", + "ref": "attend-a-fantasia-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/marrakech_attend-a-fantasia-show.jpg" + }, + { + "name": "Road to Hana", + "description": "Embark on a legendary road trip along the winding Road to Hana, stopping at cascading waterfalls, secluded beaches, and lush rainforests. Discover hidden gems like the Twin Falls, Waimoku Falls, and the black sand beach at Waiʻanapanapa State Park. Pack a picnic lunch and enjoy the breathtaking scenery along the way.", + "locationName": "Hana Highway", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "maui", + "ref": "road-to-hana", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_road-to-hana.jpg" + }, + { + "name": "Haleakala Sunrise", + "description": "Witness the awe-inspiring sunrise from the summit of Haleakala, a dormant volcano and the highest point on Maui. As the sun peeks over the horizon, casting a golden glow over the crater and the clouds below, you'll feel like you're standing above the world. Make sure to book your sunrise reservation in advance.", + "locationName": "Haleakala National Park", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "maui", + "ref": "haleakala-sunrise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_haleakala-sunrise.jpg" + }, + { + "name": "Molokini Crater Snorkeling", + "description": "Dive into the crystal-clear waters of Molokini Crater, a partially submerged volcanic caldera teeming with marine life. Snorkel or scuba dive among colorful fish, sea turtles, and vibrant coral reefs. Boat tours often include breakfast, lunch, and snorkeling equipment, making it a convenient and unforgettable experience.", + "locationName": "Molokini Crater", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "maui", + "ref": "molokini-crater-snorkeling", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_molokini-crater-snorkeling.jpg" + }, + { + "name": "Whale Watching", + "description": "During whale watching season (typically November to April), embark on a boat tour to witness the majestic humpback whales that migrate to Maui's warm waters. Marvel at their acrobatic breaches, tail slaps, and haunting songs. Knowledgeable guides will share insights about these gentle giants and their fascinating behaviors.", + "locationName": "Auau Channel", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "maui", + "ref": "whale-watching", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_whale-watching.jpg" + }, + { + "name": "Sunset at Ka'anapali Beach", + "description": "Unwind with a breathtaking sunset at Ka'anapali Beach, one of Maui's most famous stretches of sand. Relax on the beach, take a dip in the ocean, or enjoy a romantic stroll along the shore as the sky explodes with vibrant colors. Consider enjoying a delicious meal at a beachfront restaurant while soaking in the magical ambiance.", + "locationName": "Ka'anapali Beach", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "maui", + "ref": "sunset-at-ka-anapali-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_sunset-at-ka-anapali-beach.jpg" + }, + { + "name": "Iao Valley State Park Hike", + "description": "Immerse yourself in the lush beauty of Iao Valley State Park. Hike through the rainforest to the iconic Iao Needle, a towering rock formation shrouded in legend. Explore the park's diverse flora and fauna, and learn about its cultural significance to the Hawaiian people. Enjoy breathtaking views of the valley and the surrounding mountains.", + "locationName": "Iao Valley State Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "maui", + "ref": "iao-valley-state-park-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_iao-valley-state-park-hike.jpg" + }, + { + "name": "Ziplining Adventure", + "description": "Soar through the Maui rainforest on a thrilling zipline adventure. Experience the island's natural beauty from a unique perspective as you zip through the treetops. Choose from various zipline courses, offering different levels of intensity and stunning views.", + "locationName": "Various locations throughout Maui", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "maui", + "ref": "ziplining-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_ziplining-adventure.jpg" + }, + { + "name": "Maui Ocean Center", + "description": "Embark on an underwater journey at the Maui Ocean Center. Discover the diverse marine life of Hawaii through interactive exhibits, underwater tunnels, and touch pools. Learn about conservation efforts and the importance of protecting the ocean's ecosystem.", + "locationName": "Maui Ocean Center", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "maui", + "ref": "maui-ocean-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_maui-ocean-center.jpg" + }, + { + "name": "Old Lahaina Luau", + "description": "Immerse yourself in Hawaiian culture at the Old Lahaina Luau. Enjoy a traditional feast, featuring local delicacies and tropical cocktails. Witness captivating hula performances, fire dancers, and live music, while learning about Hawaiian history and traditions.", + "locationName": "Old Lahaina Luau", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "maui", + "ref": "old-lahaina-luau", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_old-lahaina-luau.jpg" + }, + { + "name": "Surfing Lessons", + "description": "Catch a wave and learn to surf on the beautiful beaches of Maui. Take a surfing lesson from experienced instructors and experience the thrill of riding the waves. Enjoy the warm waters and the stunning ocean views as you master the art of surfing.", + "locationName": "Various beaches throughout Maui", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "maui", + "ref": "surfing-lessons", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_surfing-lessons.jpg" + }, + { + "name": "Hike the Pipiwai Trail", + "description": "Embark on a breathtaking journey through bamboo forests, past cascading waterfalls, and to a stunning banyan tree on the Pipiwai Trail in Haleakala National Park. This moderate hike offers incredible views and a chance to immerse yourself in Maui's lush rainforest.", + "locationName": "Haleakala National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "maui", + "ref": "hike-the-pipiwai-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_hike-the-pipiwai-trail.jpg" + }, + { + "name": "Explore the Maui Tropical Plantation", + "description": "Discover the agricultural beauty of Maui at the Maui Tropical Plantation. Take a tram tour through fields of pineapple, sugarcane, and coffee, learn about the island's farming history, and indulge in delicious farm-to-table cuisine at the Mill House Restaurant.", + "locationName": "Maui Tropical Plantation", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "maui", + "ref": "explore-the-maui-tropical-plantation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_explore-the-maui-tropical-plantation.jpg" + }, + { + "name": "Stargazing at Haleakala", + "description": "Experience the magic of the night sky from atop Haleakala. Join a stargazing tour or find a secluded spot to marvel at the constellations, planets, and Milky Way. The high altitude and clear skies of Haleakala offer unparalleled views of the celestial wonders.", + "locationName": "Haleakala National Park", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "maui", + "ref": "stargazing-at-haleakala", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_stargazing-at-haleakala.jpg" + }, + { + "name": "Kayaking and Snorkeling Adventure", + "description": "Embark on a kayaking and snorkeling adventure along the Maui coastline. Paddle through crystal-clear waters, explore hidden coves, and discover vibrant coral reefs teeming with marine life. This activity offers a unique perspective of Maui's natural beauty.", + "locationName": "Various locations along the Maui coastline", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "maui", + "ref": "kayaking-and-snorkeling-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_kayaking-and-snorkeling-adventure.jpg" + }, + { + "name": "Attend a Traditional Hawaiian Luau", + "description": "Immerse yourself in Hawaiian culture at a traditional luau. Enjoy a feast of local cuisine, watch captivating hula performances, and learn about the island's history and traditions. This is a perfect way to experience the spirit of Aloha.", + "locationName": "Various resorts and cultural centers", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "maui", + "ref": "attend-a-traditional-hawaiian-luau", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_attend-a-traditional-hawaiian-luau.jpg" + }, + { + "name": "Helicopter Tour over Maui", + "description": "Embark on an exhilarating helicopter tour that unveils Maui's breathtaking landscapes from a unique perspective. Soar over volcanic craters, cascading waterfalls, and hidden valleys, capturing panoramic views of the island's diverse beauty.", + "locationName": "Kahului Heliport", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "maui", + "ref": "helicopter-tour-over-maui", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_helicopter-tour-over-maui.jpg" + }, + { + "name": "Biking Down Haleakala", + "description": "Experience an unforgettable adventure by biking down the slopes of Haleakala. Witness the stunning sunrise from the summit, then coast down the winding roads, enjoying breathtaking views and the thrill of the descent.", + "locationName": "Haleakala National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "maui", + "ref": "biking-down-haleakala", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_biking-down-haleakala.jpg" + }, + { + "name": "Sunset Sail with Cocktails", + "description": "Set sail on a romantic sunset cruise along the Maui coastline. Sip on handcrafted cocktails as you admire the vibrant hues of the sky, listen to live music, and enjoy the gentle ocean breeze.", + "locationName": "Lahaina Harbor", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "maui", + "ref": "sunset-sail-with-cocktails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_sunset-sail-with-cocktails.jpg" + }, + { + "name": "Maui Arts & Cultural Center", + "description": "Immerse yourself in the vibrant arts scene of Maui at the Maui Arts & Cultural Center. Enjoy live music performances, theater productions, art exhibitions, and cultural workshops that showcase the island's rich heritage.", + "locationName": "Maui Arts & Cultural Center", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "maui", + "ref": "maui-arts-cultural-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_maui-arts-cultural-center.jpg" + }, + { + "name": "Upcountry Farmers Market Hopping", + "description": "Embark on a culinary journey through Maui's Upcountry region, visiting charming farmers markets. Sample fresh local produce, artisanal cheeses, baked goods, and handcrafted treats while soaking up the relaxed atmosphere.", + "locationName": "Upcountry Maui", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "maui", + "ref": "upcountry-farmers-market-hopping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/maui_upcountry-farmers-market-hopping.jpg" + }, + { + "name": "Scenic Cruise through Milford Sound", + "description": "Embark on a breathtaking boat journey through the heart of Milford Sound. Marvel at the towering Mitre Peak, cascading waterfalls, and lush rainforests as you glide along the pristine waters. Keep an eye out for playful dolphins, fur seals, and Fiordland penguins that call this area home.", + "locationName": "Milford Sound", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "milford-sound", + "ref": "scenic-cruise-through-milford-sound", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_scenic-cruise-through-milford-sound.jpg" + }, + { + "name": "Kayaking Adventure", + "description": "Paddle through the tranquil waters of Milford Sound at your own pace, getting up close to the dramatic cliffs and waterfalls. Explore hidden coves and experience the serenity of this natural wonder from a unique perspective. Kayaking tours cater to different skill levels, making it an enjoyable activity for both beginners and experienced paddlers.", + "locationName": "Milford Sound", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "milford-sound", + "ref": "kayaking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_kayaking-adventure.jpg" + }, + { + "name": "Milford Track Hike", + "description": "Embark on a multi-day trekking adventure along the world-renowned Milford Track. This 53.5 km trail winds through stunning landscapes, including valleys, mountains, and waterfalls. Experience the thrill of crossing suspension bridges, staying in backcountry huts, and immersing yourself in the untouched beauty of Fiordland National Park.", + "locationName": "Fiordland National Park", + "duration": 48, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "milford-sound", + "ref": "milford-track-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_milford-track-hike.jpg" + }, + { + "name": "Underwater Observatory", + "description": "Descend into the depths of Milford Sound at the Milford Discovery Centre and Underwater Observatory. Observe the unique marine life, including black coral trees, sponges, and colorful fish, through large viewing windows. Learn about the fiord's ecosystem and the importance of conservation efforts.", + "locationName": "Milford Discovery Centre and Underwater Observatory", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "milford-sound", + "ref": "underwater-observatory", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_underwater-observatory.jpg" + }, + { + "name": "Scenic Flight", + "description": "Take to the skies for a breathtaking aerial perspective of Milford Sound and the surrounding Fiordland National Park. Soar above towering peaks, glaciers, and hidden valleys, capturing stunning panoramic views. This unforgettable experience offers a unique way to appreciate the grandeur of this natural wonder.", + "locationName": "Milford Sound Airport", + "duration": 1, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "milford-sound", + "ref": "scenic-flight", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_scenic-flight.jpg" + }, + { + "name": "Fiordland Discovery Centre and Underwater Observatory", + "description": "Dive into the underwater world of Milford Sound without getting wet at the Fiordland Discovery Centre and Underwater Observatory. Descend 10 meters below the surface to observe the unique marine life, including black coral, through large viewing windows. Learn about the fiord's formation, history, and delicate ecosystem through interactive exhibits. This family-friendly activity offers a fascinating glimpse into the hidden depths of Milford Sound.", + "locationName": "Fiordland Discovery Centre", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "milford-sound", + "ref": "fiordland-discovery-centre-and-underwater-observatory", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_fiordland-discovery-centre-and-underwater-observatory.jpg" + }, + { + "name": "Scenic Drive along Milford Road", + "description": "Embark on a breathtaking road trip along Milford Road, considered one of the world's most scenic drives. Wind through valleys, alongside towering mountains, and past cascading waterfalls like The Chasm and Stirling Falls. Capture stunning photos at designated lookout points, such as Eglinton Valley and Mirror Lakes. Keep an eye out for native wildlife, including kea parrots and cheeky weka birds. This self-guided adventure allows you to explore the beauty of Fiordland National Park at your own pace.", + "locationName": "Milford Road", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "milford-sound", + "ref": "scenic-drive-along-milford-road", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_scenic-drive-along-milford-road.jpg" + }, + { + "name": "Nature Walk to Lady Bowen Falls", + "description": "Immerse yourself in the lush rainforest scenery with a short and accessible walk to the base of Lady Bowen Falls. The well-maintained track leads you through vibrant native flora and offers stunning views of the fiord. Feel the refreshing spray of the falls as you approach the viewing platform. This easy walk is perfect for families and those looking for a gentle nature experience.", + "locationName": "Lady Bowen Falls Track", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "milford-sound", + "ref": "nature-walk-to-lady-bowen-falls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_nature-walk-to-lady-bowen-falls.jpg" + }, + { + "name": "Discover Maori Culture at Milford Sound Lodge", + "description": "Delve into the rich cultural heritage of the Maori people with a visit to the Milford Sound Lodge. Experience a traditional welcome ceremony, learn about Maori legends and history, and admire intricate carvings and artwork. Participate in interactive activities like weaving or carving demonstrations. This immersive cultural experience provides a deeper understanding of the Maori connection to Milford Sound and the surrounding Fiordland region.", + "locationName": "Milford Sound Lodge", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "milford-sound", + "ref": "discover-maori-culture-at-milford-sound-lodge", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_discover-maori-culture-at-milford-sound-lodge.jpg" + }, + { + "name": "Stargazing in the Sounds", + "description": "Escape the city lights and marvel at the breathtaking night sky above Milford Sound. With minimal light pollution, the fiord offers exceptional stargazing opportunities. Join a guided astronomy tour to learn about constellations, planets, and the Milky Way, or simply lie back and enjoy the celestial show. This awe-inspiring experience is perfect for a romantic evening or a unique family adventure.", + "locationName": "Milford Sound", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "milford-sound", + "ref": "stargazing-in-the-sounds", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_stargazing-in-the-sounds.jpg" + }, + { + "name": "Dive into the Depths with Scuba Diving", + "description": "Experience the magic of Milford Sound beneath the surface with a scuba diving adventure. Plunge into the deep fiord and discover a hidden world teeming with marine life, including black coral trees, colorful fish, and playful seals. Dive operators offer guided excursions for all levels, from beginners to experienced divers, ensuring a safe and unforgettable underwater journey.", + "locationName": "Various dive sites within Milford Sound", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "milford-sound", + "ref": "dive-into-the-depths-with-scuba-diving", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_dive-into-the-depths-with-scuba-diving.jpg" + }, + { + "name": "Fly Fishing in Pristine Waters", + "description": "Cast your line amidst the stunning scenery of Milford Sound and try your hand at fly fishing. The area is renowned for its brown and rainbow trout, providing an exciting challenge for anglers of all skill levels. Local guides can lead you to the best fishing spots and offer expert advice, making for a rewarding and tranquil experience in nature.", + "locationName": "Cleddau River or Lake Te Anau", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "milford-sound", + "ref": "fly-fishing-in-pristine-waters", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_fly-fishing-in-pristine-waters.jpg" + }, + { + "name": "Picnic with a View at Mitre Peak", + "description": "Pack a delightful picnic basket and head to the base of the iconic Mitre Peak for an unforgettable dining experience. Find a scenic spot along the shoreline or amidst the lush greenery, and savor your meal while marveling at the towering peak and the surrounding natural beauty. It's the perfect way to relax and soak in the tranquility of Milford Sound.", + "locationName": "Mitre Peak", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "milford-sound", + "ref": "picnic-with-a-view-at-mitre-peak", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_picnic-with-a-view-at-mitre-peak.jpg" + }, + { + "name": "Birdwatching Paradise", + "description": "Embark on a birdwatching expedition and discover the diverse avian species that call Milford Sound home. Keep an eye out for rare birds like the Fiordland penguin, the kea (a playful alpine parrot), and the South Island robin. Explore the various walking trails and hidden coves to spot these feathered wonders in their natural habitat.", + "locationName": "Fiordland National Park", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "milford-sound", + "ref": "birdwatching-paradise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_birdwatching-paradise.jpg" + }, + { + "name": "Capture the Beauty: Photography Tour", + "description": "Join a photography tour led by a local expert and learn how to capture the breathtaking landscapes of Milford Sound. Discover the best vantage points, lighting techniques, and composition tips to create stunning images that will preserve your memories of this magical place. Whether you're a seasoned photographer or a novice, this tour offers a unique perspective and an opportunity to hone your skills.", + "locationName": "Various locations throughout Milford Sound", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "milford-sound", + "ref": "capture-the-beauty-photography-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_capture-the-beauty-photography-tour.jpg" + }, + { + "name": "Canyoning Adventure", + "description": "Embark on a thrilling canyoning experience in the heart of Fiordland National Park. Rappel down cascading waterfalls, plunge into crystal-clear pools, and navigate through narrow gorges, surrounded by breathtaking scenery. This adrenaline-pumping activity is perfect for adventure seekers.", + "locationName": "Cleddau Canyon", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "milford-sound", + "ref": "canyoning-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_canyoning-adventure.jpg" + }, + { + "name": "Milford Sound Foreshore Walk", + "description": "Take a leisurely stroll along the Milford Sound foreshore and immerse yourself in the tranquility of nature. This easy walking trail offers stunning views of the fiord, lush rainforest, and Mitre Peak. Keep an eye out for native birds and marine life, including seals and penguins.", + "locationName": "Milford Sound Foreshore", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "milford-sound", + "ref": "milford-sound-foreshore-walk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_milford-sound-foreshore-walk.jpg" + }, + { + "name": "Te Anau Glowworm Caves", + "description": "Embark on a magical journey through the Te Anau Glowworm Caves, a subterranean wonderland illuminated by thousands of tiny glowworms. Take a boat ride through the darkness and marvel at the twinkling lights, while learning about the unique ecosystem of these fascinating creatures.", + "locationName": "Te Anau", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "milford-sound", + "ref": "te-anau-glowworm-caves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_te-anau-glowworm-caves.jpg" + }, + { + "name": "Horseback Riding through Fiordland", + "description": "Explore the stunning landscapes of Fiordland National Park on horseback. Ride through native forests, alongside pristine rivers, and enjoy panoramic views of the surrounding mountains. This activity is suitable for all levels of experience and offers a unique perspective of the area.", + "locationName": "Fiordland National Park", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "milford-sound", + "ref": "horseback-riding-through-fiordland", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_horseback-riding-through-fiordland.jpg" + }, + { + "name": "Relaxing Spa Day", + "description": "Indulge in a day of pampering and rejuvenation at a luxurious spa. Choose from a range of treatments, including massages, facials, and body wraps, and let your stress melt away amidst the serene surroundings. This is the perfect way to unwind after a day of exploring.", + "locationName": "Milford Sound Lodge", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "milford-sound", + "ref": "relaxing-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/milford-sound_relaxing-spa-day.jpg" + }, + { + "name": "Hiking in Arches National Park", + "description": "Embark on a breathtaking hike through Arches National Park, home to over 2,000 natural sandstone arches. Explore iconic formations like Delicate Arch, Landscape Arch, and Double Arch, and witness the awe-inspiring power of nature. Trails range from easy to challenging, offering options for all fitness levels.", + "locationName": "Arches National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "moab", + "ref": "hiking-in-arches-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_hiking-in-arches-national-park.jpg" + }, + { + "name": "Mountain Biking the Slickrock Trail", + "description": "Experience the thrill of mountain biking on the world-renowned Slickrock Trail. This challenging route features steep inclines, slickrock surfaces, and stunning views of the Moab landscape. Test your skills and endurance as you navigate this iconic trail.", + "locationName": "Slickrock Bike Trail", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "moab", + "ref": "mountain-biking-the-slickrock-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_mountain-biking-the-slickrock-trail.jpg" + }, + { + "name": "White-Water Rafting on the Colorado River", + "description": "Embark on an exhilarating white-water rafting adventure on the Colorado River. Navigate through rapids, admire the towering canyon walls, and experience the thrill of this iconic river. Choose from various trip lengths and difficulty levels to suit your preferences.", + "locationName": "Colorado River", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "moab", + "ref": "white-water-rafting-on-the-colorado-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_white-water-rafting-on-the-colorado-river.jpg" + }, + { + "name": "Off-Roading in Canyonlands National Park", + "description": "Explore the rugged backcountry of Canyonlands National Park on an off-roading adventure. Discover hidden canyons, ancient ruins, and breathtaking viewpoints as you navigate the challenging terrain. This thrilling experience offers a unique perspective of the park's vast landscapes.", + "locationName": "Canyonlands National Park", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "moab", + "ref": "off-roading-in-canyonlands-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_off-roading-in-canyonlands-national-park.jpg" + }, + { + "name": "Stargazing in the Desert", + "description": "Escape the city lights and experience the magic of stargazing in the Moab desert. With minimal light pollution, the night sky comes alive with countless stars, constellations, and even the Milky Way. Join a guided stargazing tour or find a secluded spot to marvel at the celestial wonders above.", + "locationName": "Moab Desert", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "moab", + "ref": "stargazing-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_stargazing-in-the-desert.jpg" + }, + { + "name": "Canyoneering", + "description": "Embark on a thrilling canyoneering adventure, rappelling down sandstone cliffs, navigating slot canyons, and exploring hidden waterfalls. This activity offers a unique perspective of Moab's rugged landscapes and is perfect for adrenaline seekers.", + "locationName": "Various canyons around Moab", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "moab", + "ref": "canyoneering", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_canyoneering.jpg" + }, + { + "name": "Scenic Drives", + "description": "Take a leisurely drive along scenic byways like the Potash Road or La Sal Mountain Loop, offering breathtaking views of red rock formations, canyons, and the La Sal Mountains. Enjoy photo opportunities and picnics at designated stops.", + "locationName": "Potash Road, La Sal Mountain Loop", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "moab", + "ref": "scenic-drives", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_scenic-drives.jpg" + }, + { + "name": "Rock Climbing", + "description": "Challenge yourself with rock climbing on Moab's iconic sandstone cliffs. With routes for all skill levels, from beginner to expert, experience the thrill of scaling vertical walls and enjoying panoramic views.", + "locationName": "Various climbing areas around Moab", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "moab", + "ref": "rock-climbing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_rock-climbing.jpg" + }, + { + "name": "Dinosaur Museum", + "description": "Explore the Moab Museum, which houses fascinating exhibits on the region's rich paleontological history. Discover dinosaur fossils, learn about ancient cultures, and delve into the geological wonders of the area.", + "locationName": "Moab Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "moab", + "ref": "dinosaur-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_dinosaur-museum.jpg" + }, + { + "name": "Hot Air Balloon Ride", + "description": "Experience the magic of a hot air balloon ride over Moab's breathtaking landscapes. Soar above red rock formations, canyons, and the Colorado River, enjoying panoramic views and a unique perspective of the desert.", + "locationName": "Moab Valley", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "moab", + "ref": "hot-air-balloon-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_hot-air-balloon-ride.jpg" + }, + { + "name": "Horseback Riding in the Desert", + "description": "Embark on a scenic horseback riding adventure through the rugged desert landscape surrounding Moab. Local outfitters offer guided tours suitable for all skill levels, allowing you to explore hidden canyons, ancient ruins, and panoramic vistas at a relaxed pace. This activity is a unique way to connect with nature and experience the Old West charm of Moab.", + "locationName": "Various locations around Moab", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "moab", + "ref": "horseback-riding-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_horseback-riding-in-the-desert.jpg" + }, + { + "name": "Scenic Flight Over Canyonlands National Park", + "description": "Take to the skies for a breathtaking aerial tour of Canyonlands National Park. Soar above the vast canyons, mesas, and rivers, gaining a unique perspective of the park's iconic landmarks like Mesa Arch, the Green River Overlook, and the Needles district. This unforgettable experience offers stunning photo opportunities and a chance to appreciate the sheer scale and beauty of the desert landscape.", + "locationName": "Canyonlands National Park", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "moab", + "ref": "scenic-flight-over-canyonlands-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_scenic-flight-over-canyonlands-national-park.jpg" + }, + { + "name": "Relaxation and Rejuvenation at a Spa", + "description": "After a day of adventure, unwind and indulge in a pampering spa experience. Moab offers several wellness centers and spas where you can enjoy a variety of treatments, including massages, facials, body wraps, and hydrotherapy. Some spas even incorporate local elements like red rock clay or desert botanicals into their treatments, providing a unique and rejuvenating experience.", + "locationName": "Various spas in Moab", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "moab", + "ref": "relaxation-and-rejuvenation-at-a-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_relaxation-and-rejuvenation-at-a-spa.jpg" + }, + { + "name": "Explore Moab's Culinary Scene", + "description": "Discover the diverse flavors of Moab's culinary scene. From casual cafes and breweries to fine dining establishments, Moab offers a range of options to satisfy any palate. Sample Southwestern specialties, indulge in wood-fired pizzas, or savor fresh, locally sourced ingredients. Be sure to try the craft beers brewed in Moab and explore the charming cafes and restaurants along Main Street.", + "locationName": "Various restaurants and cafes in Moab", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "moab", + "ref": "explore-moab-s-culinary-scene", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_explore-moab-s-culinary-scene.jpg" + }, + { + "name": "Attend a Moab Music Festival Performance", + "description": "Immerse yourself in the vibrant arts and culture scene of Moab by attending a performance at the Moab Music Festival. This renowned festival features world-class musicians performing chamber music, jazz, and other genres in stunning outdoor settings amidst the red rock landscapes. Enjoy an evening of exceptional music under the stars, surrounded by the natural beauty of Moab.", + "locationName": "Various locations in Moab", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "moab", + "ref": "attend-a-moab-music-festival-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_attend-a-moab-music-festival-performance.jpg" + }, + { + "name": "Jeep Safari Tour to Dead Horse Point State Park", + "description": "Embark on a thrilling jeep safari tour to Dead Horse Point State Park, renowned for its breathtaking panoramic views of the Colorado River and Canyonlands National Park. Traverse rugged trails, learn about the area's geology and history, and capture stunning photographs of the iconic landscapes.", + "locationName": "Dead Horse Point State Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "moab", + "ref": "jeep-safari-tour-to-dead-horse-point-state-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_jeep-safari-tour-to-dead-horse-point-state-park.jpg" + }, + { + "name": "Stand Up Paddleboarding on the Colorado River", + "description": "Experience the serenity of the Colorado River from a unique perspective with stand-up paddleboarding. Glide along calm stretches of the river, surrounded by towering red rock cliffs, and enjoy the tranquility of nature. This activity is suitable for all skill levels and offers a relaxing way to explore the waterways.", + "locationName": "Colorado River", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "moab", + "ref": "stand-up-paddleboarding-on-the-colorado-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_stand-up-paddleboarding-on-the-colorado-river.jpg" + }, + { + "name": "Visit the Moab Museum", + "description": "Delve into the rich history and culture of Moab at the Moab Museum. Discover exhibits that showcase the region's Native American heritage, pioneer settlements, uranium mining boom, and the development of Moab as an adventure tourism destination. Gain insights into the area's unique past and present.", + "locationName": "Moab Museum", + "duration": 1.5, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "moab", + "ref": "visit-the-moab-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_visit-the-moab-museum.jpg" + }, + { + "name": "Sunset Horseback Riding Adventure", + "description": "Saddle up for a magical horseback riding adventure as the sun sets over the Moab desert. Traverse scenic trails, witness the vibrant colors of the sky, and enjoy the peaceful ambiance of the evening. This experience is perfect for creating lasting memories.", + "locationName": "Moab desert trails", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "moab", + "ref": "sunset-horseback-riding-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_sunset-horseback-riding-adventure.jpg" + }, + { + "name": "Moab Brewery Tour and Tasting", + "description": "Indulge in the local craft beer scene with a tour and tasting at Moab Brewery. Learn about the brewing process, sample a variety of beers, and discover the unique flavors of Moab. This activity is ideal for beer enthusiasts and those seeking a relaxed evening experience.", + "locationName": "Moab Brewery", + "duration": 1.5, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "moab", + "ref": "moab-brewery-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/moab_moab-brewery-tour-and-tasting.jpg" + }, + { + "name": "Explore the Bay of Kotor", + "description": "Embark on a scenic boat tour around the Bay of Kotor, a UNESCO World Heritage Site. Marvel at the dramatic landscapes, charming coastal towns, and historical landmarks like Our Lady of the Rocks and the ancient city walls of Kotor.", + "locationName": "Bay of Kotor", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "montenegro", + "ref": "explore-the-bay-of-kotor", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_explore-the-bay-of-kotor.jpg" + }, + { + "name": "Hiking in Durmitor National Park", + "description": "Lace up your hiking boots and venture into the stunning Durmitor National Park. Hike through pristine forests, glacial lakes, and rugged peaks, including Bobotov Kuk, the highest peak in Montenegro. Enjoy breathtaking views and immerse yourself in nature.", + "locationName": "Durmitor National Park", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "montenegro", + "ref": "hiking-in-durmitor-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_hiking-in-durmitor-national-park.jpg" + }, + { + "name": "Relax on the Beaches of Budva", + "description": "Soak up the sun on the beautiful beaches of Budva, a popular coastal town known for its vibrant atmosphere. Enjoy swimming, sunbathing, or trying out water sports like kayaking and paddleboarding. In the evening, explore the charming Old Town and enjoy the lively nightlife.", + "locationName": "Budva", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "montenegro", + "ref": "relax-on-the-beaches-of-budva", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_relax-on-the-beaches-of-budva.jpg" + }, + { + "name": "Discover the Historic City of Kotor", + "description": "Step back in time and explore the historic city of Kotor. Wander through the labyrinthine streets of the Old Town, visit the Cathedral of Saint Tryphon, and climb the ancient city walls for panoramic views of the bay. Immerse yourself in the rich culture and history of this charming town.", + "locationName": "Kotor", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "montenegro", + "ref": "discover-the-historic-city-of-kotor", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_discover-the-historic-city-of-kotor.jpg" + }, + { + "name": "Experience Local Cuisine and Wine", + "description": "Indulge in the delicious flavors of Montenegrin cuisine. Sample fresh seafood specialties, hearty meat dishes, and local cheeses. Pair your meal with a glass of Montenegrin wine, known for its unique varietals and rich flavors. Visit a local winery or enjoy a traditional meal at a family-run restaurant.", + "locationName": "Various", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "montenegro", + "ref": "experience-local-cuisine-and-wine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_experience-local-cuisine-and-wine.jpg" + }, + { + "name": "Whitewater Rafting on the Tara River", + "description": "Embark on an exhilarating adventure through Europe's deepest canyon, the Tara River Canyon. Navigate thrilling rapids, surrounded by stunning landscapes, and experience the power of nature up close. Choose from various difficulty levels and enjoy a day of adrenaline and breathtaking scenery.", + "locationName": "Tara River Canyon", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "montenegro", + "ref": "whitewater-rafting-on-the-tara-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_whitewater-rafting-on-the-tara-river.jpg" + }, + { + "name": "Kayaking in the Bay of Kotor", + "description": "Explore the tranquil waters of the Bay of Kotor from a different perspective. Paddle along the coastline, admiring the picturesque villages, historic fortifications, and dramatic mountains that enclose the bay. Discover hidden coves, secluded beaches, and enjoy a peaceful escape surrounded by natural beauty.", + "locationName": "Bay of Kotor", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "montenegro", + "ref": "kayaking-in-the-bay-of-kotor", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_kayaking-in-the-bay-of-kotor.jpg" + }, + { + "name": "Wine Tasting Tour in Crmnica Region", + "description": "Indulge in the rich flavors of Montenegro's wine culture with a visit to the Crmnica region. Explore local vineyards, learn about traditional winemaking techniques, and sample a variety of indigenous grape varieties. Discover the unique characteristics of Vranac, Krstač, and other local wines, while enjoying the scenic beauty of the countryside.", + "locationName": "Crmnica Region", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "montenegro", + "ref": "wine-tasting-tour-in-crmnica-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_wine-tasting-tour-in-crmnica-region.jpg" + }, + { + "name": "Paragliding over Budva Riviera", + "description": "Soar above the stunning Budva Riviera and experience breathtaking panoramic views of the Adriatic coastline. Take off from the mountains and glide through the air, enjoying the sensation of freedom and admiring the beauty of the beaches, islands, and surrounding landscapes. This unforgettable adventure offers a unique perspective of Montenegro's coastal charm.", + "locationName": "Budva Riviera", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "montenegro", + "ref": "paragliding-over-budva-riviera", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_paragliding-over-budva-riviera.jpg" + }, + { + "name": "Exploring Skadar Lake National Park", + "description": "Discover the natural wonders of Skadar Lake National Park, the largest lake in the Balkans. Take a boat trip to explore the lake's diverse ecosystem, home to numerous bird species, including pelicans and herons. Visit traditional fishing villages, ancient monasteries, and hidden beaches, and immerse yourself in the tranquility of this unique natural haven.", + "locationName": "Skadar Lake National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "montenegro", + "ref": "exploring-skadar-lake-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_exploring-skadar-lake-national-park.jpg" + }, + { + "name": "Canyoning in Nevidio Canyon", + "description": "Embark on an exhilarating canyoning adventure through the Nevidio Canyon, known for its narrow passages, cascading waterfalls, and stunning natural beauty. This activity involves rappelling, swimming, and jumping through the canyon, providing an unforgettable adrenaline rush. ", + "locationName": "Nevidio Canyon", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "montenegro", + "ref": "canyoning-in-nevidio-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_canyoning-in-nevidio-canyon.jpg" + }, + { + "name": "Horseback Riding in the Mountains", + "description": "Explore the scenic landscapes of Montenegro's mountains on horseback, enjoying breathtaking views and a peaceful connection with nature. Choose from various trails suitable for all levels, from gentle rides through meadows to more challenging routes with panoramic vistas.", + "locationName": "Durmitor National Park or Lovcen National Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "montenegro", + "ref": "horseback-riding-in-the-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_horseback-riding-in-the-mountains.jpg" + }, + { + "name": "Visit the Ostrog Monastery", + "description": "Discover the spiritual and architectural marvel of Ostrog Monastery, a Serbian Orthodox monastery carved into a cliff face. Witness stunning views, explore the monastery complex, and learn about its religious significance and history.", + "locationName": "Ostrog Monastery", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "montenegro", + "ref": "visit-the-ostrog-monastery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_visit-the-ostrog-monastery.jpg" + }, + { + "name": "Explore the Old Town of Budva", + "description": "Wander through the charming streets of Budva's Old Town, a historic fortified city with Venetian-era architecture, quaint shops, and lively cafes. Discover hidden squares, ancient churches, and the Citadel for panoramic views of the Adriatic Sea.", + "locationName": "Budva Old Town", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "montenegro", + "ref": "explore-the-old-town-of-budva", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_explore-the-old-town-of-budva.jpg" + }, + { + "name": "Enjoy the Nightlife in Budva or Kotor", + "description": "Experience Montenegro's vibrant nightlife scene in Budva or Kotor. Choose from a variety of bars, clubs, and restaurants offering live music, DJs, and entertainment. Dance the night away or relax with a drink while enjoying the coastal ambiance.", + "locationName": "Budva or Kotor", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "montenegro", + "ref": "enjoy-the-nightlife-in-budva-or-kotor", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_enjoy-the-nightlife-in-budva-or-kotor.jpg" + }, + { + "name": "Ziplining in Lovcen National Park", + "description": "Experience an adrenaline rush as you soar through the treetops of Lovcen National Park on a thrilling zipline adventure. Take in breathtaking views of the surrounding mountains and valleys as you zip from platform to platform. This activity is perfect for adventure seekers and nature lovers.", + "locationName": "Lovcen National Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "montenegro", + "ref": "ziplining-in-lovcen-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_ziplining-in-lovcen-national-park.jpg" + }, + { + "name": "Scuba Diving in the Adriatic Sea", + "description": "Dive into the crystal-clear waters of the Adriatic Sea and discover a vibrant underwater world teeming with marine life. Explore shipwrecks, reefs, and caves, and encounter colorful fish, octopuses, and other fascinating creatures.", + "locationName": "Adriatic Sea", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "montenegro", + "ref": "scuba-diving-in-the-adriatic-sea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_scuba-diving-in-the-adriatic-sea.jpg" + }, + { + "name": "Visit the Njagara Waterfalls", + "description": "Embark on a scenic hike to the Njagara Waterfalls, a hidden gem nestled amidst lush greenery. Witness the cascading water as it plunges into a picturesque pool below, and enjoy a refreshing dip in the cool waters.", + "locationName": "Njagara Waterfalls", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "montenegro", + "ref": "visit-the-njagara-waterfalls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_visit-the-njagara-waterfalls.jpg" + }, + { + "name": "Take a Boat Trip to Sveti Stefan", + "description": "Embark on a boat trip to the exclusive island of Sveti Stefan, a luxurious resort known for its stunning beaches and historical architecture. Explore the island's narrow streets, relax on the beach, and soak up the glamorous atmosphere.", + "locationName": "Sveti Stefan", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "montenegro", + "ref": "take-a-boat-trip-to-sveti-stefan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_take-a-boat-trip-to-sveti-stefan.jpg" + }, + { + "name": "Birdwatching in Lake Skadar National Park", + "description": "Discover the diverse avian population of Lake Skadar National Park, a haven for birdwatchers. Spot pelicans, herons, egrets, and other species as you explore the wetlands, marshes, and open waters of the park.", + "locationName": "Lake Skadar National Park", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "montenegro", + "ref": "birdwatching-in-lake-skadar-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/montenegro_birdwatching-in-lake-skadar-national-park.jpg" + }, + { + "name": "Riesling Wine Tour and Tasting", + "description": "Embark on a delightful journey through the renowned vineyards of the Mosel Valley. Visit charming local wineries, meet passionate winemakers, and indulge in tastings of the region's celebrated Riesling wines. Learn about the unique terroir and winemaking traditions that contribute to the exceptional quality of Mosel wines.", + "locationName": "Various wineries along the Mosel River", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "mosel-valley", + "ref": "riesling-wine-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_riesling-wine-tour-and-tasting.jpg" + }, + { + "name": "Cochem Castle Exploration", + "description": "Step back in time with a visit to the majestic Cochem Castle, perched high above the Mosel River. Explore the medieval architecture, wander through historic halls, and enjoy panoramic views of the surrounding valley. Immerse yourself in the rich history and captivating legends of this iconic landmark.", + "locationName": "Cochem Castle", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "mosel-valley", + "ref": "cochem-castle-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_cochem-castle-exploration.jpg" + }, + { + "name": "Scenic Hiking along the Mosel River", + "description": "Lace up your hiking boots and embark on a picturesque journey along the Mosel River. Follow well-maintained trails that wind through rolling vineyards, charming villages, and breathtaking natural landscapes. Enjoy panoramic views of the river valley and immerse yourself in the tranquility of the surrounding nature.", + "locationName": "Moselsteig Trail", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "mosel-valley", + "ref": "scenic-hiking-along-the-mosel-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_scenic-hiking-along-the-mosel-river.jpg" + }, + { + "name": "Relaxing River Cruise", + "description": "Sit back, relax, and soak in the beauty of the Mosel Valley on a leisurely river cruise. Glide along the tranquil waters, admiring the picturesque vineyards, charming villages, and historic castles that line the riverbanks. Enjoy onboard commentary and learn about the region's history and culture while indulging in delicious local cuisine and wine.", + "locationName": "Mosel River", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mosel-valley", + "ref": "relaxing-river-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_relaxing-river-cruise.jpg" + }, + { + "name": "Beilstein Village Stroll", + "description": "Wander through the enchanting village of Beilstein, known as the \"Sleeping Beauty of the Mosel.\" Explore its narrow cobblestone streets, admire the half-timbered houses, and discover hidden courtyards and charming cafes. Enjoy the peaceful atmosphere and soak in the timeless beauty of this historic village.", + "locationName": "Beilstein Village", + "duration": 1, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "mosel-valley", + "ref": "beilstein-village-stroll", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_beilstein-village-stroll.jpg" + }, + { + "name": "Kayaking on the Mosel River", + "description": "Experience the Mosel Valley's beauty from a unique perspective by kayaking down the serene Mosel River. Paddle past rolling vineyards, charming villages, and towering castles, immersing yourself in the tranquility of the surrounding nature. Rent a kayak for a few hours or embark on a guided tour to explore hidden coves and learn about the region's rich history and ecology. This activity is suitable for various skill levels and offers a refreshing way to enjoy the outdoors.", + "locationName": "Mosel River", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "mosel-valley", + "ref": "kayaking-on-the-mosel-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_kayaking-on-the-mosel-river.jpg" + }, + { + "name": "Cycling through the Vineyards", + "description": "Embark on a scenic cycling adventure through the picturesque vineyards of the Mosel Valley. Rent a bike and follow well-maintained paths that wind through rolling hills, offering breathtaking views of the river and surrounding landscapes. Stop at local wineries for tastings, savor delicious regional cuisine at charming cafes, and discover hidden gems along the way. This activity allows you to explore the region at your own pace and soak in the beauty of the vineyards.", + "locationName": "Mosel Valley vineyards", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "mosel-valley", + "ref": "cycling-through-the-vineyards", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_cycling-through-the-vineyards.jpg" + }, + { + "name": "Burg Eltz Exploration", + "description": "Step back in time with a visit to the enchanting Burg Eltz, a medieval castle nestled amidst the forested hills above the Mosel River. Explore the castle's impressive architecture, admire its well-preserved interiors, and learn about its fascinating history dating back to the 12th century. Take a guided tour or wander through the castle's chambers, armory, and treasury at your own pace. The surrounding forest offers scenic hiking trails with breathtaking views of the castle and valley.", + "locationName": "Burg Eltz", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mosel-valley", + "ref": "burg-eltz-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_burg-eltz-exploration.jpg" + }, + { + "name": "Indulge in German Cuisine", + "description": "Embark on a culinary journey through the Mosel Valley, savoring authentic German dishes and local specialties. Visit traditional restaurants and cozy taverns to experience the region's rich gastronomic heritage. Sample hearty meat dishes, fresh seafood, and seasonal vegetables, paired with locally produced wines. Don't miss the opportunity to try regional favorites like Zwiebelkuchen (onion cake) and Kartoffelpuffer (potato pancakes).", + "locationName": "Various restaurants and taverns throughout the Mosel Valley", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "mosel-valley", + "ref": "indulge-in-german-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_indulge-in-german-cuisine.jpg" + }, + { + "name": "Thermal Baths and Wellness Retreats", + "description": "Unwind and rejuvenate at one of the Mosel Valley's renowned thermal baths or wellness retreats. Immerse yourself in the healing mineral waters, indulge in spa treatments, and experience relaxation like never before. Many facilities offer a range of services, including massages, saunas, and beauty treatments. Whether you seek a day of pampering or a complete wellness getaway, the Mosel Valley offers options for every preference.", + "locationName": "Various thermal baths and wellness centers throughout the Mosel Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "mosel-valley", + "ref": "thermal-baths-and-wellness-retreats", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_thermal-baths-and-wellness-retreats.jpg" + }, + { + "name": "Hot Air Balloon Ride over the Vineyards", + "description": "Experience the breathtaking beauty of the Mosel Valley from a unique perspective with a hot air balloon ride. Soar above the rolling vineyards, charming villages, and meandering river as you enjoy panoramic views of the picturesque landscape. This unforgettable experience offers a romantic and serene way to appreciate the region's charm.", + "locationName": "Various locations along the Mosel Valley", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "mosel-valley", + "ref": "hot-air-balloon-ride-over-the-vineyards", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_hot-air-balloon-ride-over-the-vineyards.jpg" + }, + { + "name": "Photography Tour of the Mosel Valley", + "description": "Capture the essence of the Mosel Valley's beauty on a photography tour led by a local expert. Discover hidden gems, perfect vantage points, and charming details as you explore the vineyards, villages, and riverbanks. Learn about composition, lighting, and storytelling through photography while creating lasting memories of your trip. #Great for photography #Instagrammable", + "locationName": "Various locations along the Mosel Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mosel-valley", + "ref": "photography-tour-of-the-mosel-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_photography-tour-of-the-mosel-valley.jpg" + }, + { + "name": "Birdwatching in the Mosel Forests", + "description": "Escape the bustling towns and immerse yourself in the tranquility of the Mosel forests. Embark on a guided birdwatching tour to spot diverse avian species, including woodpeckers, owls, and songbirds. Learn about the region's ecosystem and the importance of conservation while enjoying the peaceful sounds of nature. #Wildlife watching", + "locationName": "Forests surrounding the Mosel Valley", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "mosel-valley", + "ref": "birdwatching-in-the-mosel-forests", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_birdwatching-in-the-mosel-forests.jpg" + }, + { + "name": "Folklore and Fairytale Evening", + "description": "Delve into the rich cultural heritage of the Mosel Valley with an enchanting evening of folklore and fairytales. Gather around a bonfire as local storytellers share captivating legends and myths passed down through generations. Enjoy traditional music, dance, and perhaps even a taste of regional delicacies for a truly immersive experience. #Cultural experiences", + "locationName": "Various villages in the Mosel Valley", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "mosel-valley", + "ref": "folklore-and-fairytale-evening", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_folklore-and-fairytale-evening.jpg" + }, + { + "name": "E-bike Vineyard Adventure", + "description": "Explore the Mosel Valley's vineyards with ease and excitement on an e-bike adventure. Rent an e-bike and follow designated routes through the vineyards, stopping at charming villages and wineries along the way. Enjoy the flexibility to explore at your own pace while taking in the stunning scenery and fresh air. #Adventure sports", + "locationName": "Vineyards throughout the Mosel Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "mosel-valley", + "ref": "e-bike-vineyard-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_e-bike-vineyard-adventure.jpg" + }, + { + "name": "Underground History and Mystery", + "description": "Embark on a captivating journey into the depths of the Mosel Valley, exploring the region's hidden underground world. Visit ancient Roman wine cellars, medieval mining tunnels, and mysterious underground fortifications. Discover the secrets and stories buried beneath the surface, and gain a unique perspective on the Mosel Valley's rich history.", + "locationName": "Various locations throughout the Mosel Valley", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "mosel-valley", + "ref": "underground-history-and-mystery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_underground-history-and-mystery.jpg" + }, + { + "name": "Culinary Delights: Cooking Class and Market Tour", + "description": "Immerse yourself in the flavors of the Mosel Valley with a hands-on cooking class and local market tour. Join a skilled chef to learn the art of preparing traditional German dishes, using fresh, seasonal ingredients sourced from the vibrant markets. Discover the secrets of regional specialties and enjoy the fruits (and vegetables) of your labor with a delicious meal.", + "locationName": "Various towns and villages along the Mosel River", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "mosel-valley", + "ref": "culinary-delights-cooking-class-and-market-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_culinary-delights-cooking-class-and-market-tour.jpg" + }, + { + "name": "Art and Culture: Visit Local Galleries and Museums", + "description": "Delve into the artistic and cultural heritage of the Mosel Valley by exploring its charming galleries and museums. Discover a diverse range of art forms, from contemporary paintings and sculptures to historical artifacts and regional crafts. Immerse yourself in the creative spirit of the region and gain insights into its rich cultural tapestry.", + "locationName": "Various towns and villages along the Mosel River", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "mosel-valley", + "ref": "art-and-culture-visit-local-galleries-and-museums", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_art-and-culture-visit-local-galleries-and-museums.jpg" + }, + { + "name": "Stargazing on the Mosel", + "description": "Escape the city lights and experience the magic of the night sky over the Mosel Valley. Join a guided stargazing tour or find a secluded spot along the riverbank to marvel at the constellations. Learn about the mythology and science behind the stars, and enjoy the tranquility of the night.", + "locationName": "Various locations along the Mosel River", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "mosel-valley", + "ref": "stargazing-on-the-mosel", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_stargazing-on-the-mosel.jpg" + }, + { + "name": "Festival Fun: Experience Local Traditions", + "description": "Immerse yourself in the vibrant culture of the Mosel Valley by attending one of the many local festivals throughout the year. From wine festivals and harvest celebrations to traditional folk events and Christmas markets, there's always something happening in the region. Enjoy live music, delicious food, and a festive atmosphere as you experience the local traditions and customs.", + "locationName": "Various towns and villages along the Mosel River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "mosel-valley", + "ref": "festival-fun-experience-local-traditions", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mosel-valley_festival-fun-experience-local-traditions.jpg" + }, + { + "name": "Sunrise over Bagan", + "description": "Witness the breathtaking sight of hundreds of ancient temples bathed in the golden glow of dawn. Take a hot air balloon ride or climb to the top of a temple for panoramic views of the plains dotted with these historical marvels. This unforgettable experience is a photographer's dream and a must-do for any visitor to Myanmar.", + "locationName": "Bagan Archaeological Zone", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "myanmar", + "ref": "sunrise-over-bagan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_sunrise-over-bagan.jpg" + }, + { + "name": "Explore Shwedagon Pagoda", + "description": "Immerse yourself in the spiritual heart of Myanmar at the iconic Shwedagon Pagoda. Marvel at the gold-plated stupa, adorned with thousands of diamonds and precious stones. Wander around the complex, observing locals in prayer and learning about Buddhist traditions. The serene atmosphere and intricate architecture offer a glimpse into Myanmar's rich religious heritage.", + "locationName": "Yangon", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "myanmar", + "ref": "explore-shwedagon-pagoda", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_explore-shwedagon-pagoda.jpg" + }, + { + "name": "Cruise the Irrawaddy River", + "description": "Embark on a scenic journey along the Irrawaddy River, the lifeblood of Myanmar. Sail past rural villages, verdant landscapes, and ancient temples. Opt for a multi-day cruise to explore remote areas and experience the local way of life. This relaxing adventure allows you to witness the beauty and cultural diversity of Myanmar from a unique perspective.", + "locationName": "Irrawaddy River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "myanmar", + "ref": "cruise-the-irrawaddy-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_cruise-the-irrawaddy-river.jpg" + }, + { + "name": "Trekking in Hsipaw", + "description": "Escape the tourist trail and venture into the scenic hills of Hsipaw. Embark on a multi-day trek through lush rice paddies, remote villages, and stunning waterfalls. Stay overnight in local homestays, experiencing the warm hospitality of the Shan people. This off-the-beaten-path adventure offers a unique insight into rural life in Myanmar.", + "locationName": "Hsipaw", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "myanmar", + "ref": "trekking-in-hsipaw", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_trekking-in-hsipaw.jpg" + }, + { + "name": "Discover Inle Lake", + "description": "Explore the serene beauty of Inle Lake, a vast freshwater lake surrounded by mountains and floating gardens. Take a boat trip to observe the unique leg-rowing fishermen and visit stilt villages where local artisans practice traditional crafts. Immerse yourself in the tranquility of this picturesque landscape and learn about the fascinating culture of the Intha people.", + "locationName": "Inle Lake", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "myanmar", + "ref": "discover-inle-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_discover-inle-lake.jpg" + }, + { + "name": "Visit the Golden Rock", + "description": "Embark on a pilgrimage to the Kyaiktiyo Pagoda, also known as the Golden Rock, a gravity-defying boulder covered in gold leaf and perched precariously on a cliff edge. The journey involves a truck ride up the mountain and a short walk to the pagoda, offering breathtaking views and a unique spiritual experience.", + "locationName": "Kyaiktiyo", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "myanmar", + "ref": "visit-the-golden-rock", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_visit-the-golden-rock.jpg" + }, + { + "name": "Explore Mandalay", + "description": "Discover the cultural heart of Myanmar in Mandalay. Visit the Royal Palace, climb Mandalay Hill for panoramic views, and explore the Kuthodaw Pagoda, home to the world's largest book. Immerse yourself in the local life at bustling markets and enjoy traditional puppet shows.", + "locationName": "Mandalay", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "myanmar", + "ref": "explore-mandalay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_explore-mandalay.jpg" + }, + { + "name": "Relax on Ngapali Beach", + "description": "Escape to the pristine shores of Ngapali Beach, known for its turquoise waters, soft sand, and swaying palm trees. Enjoy swimming, sunbathing, or simply unwinding with a good book. Indulge in fresh seafood at beachside restaurants and experience the laid-back charm of this coastal paradise.", + "locationName": "Ngapali Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "myanmar", + "ref": "relax-on-ngapali-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_relax-on-ngapali-beach.jpg" + }, + { + "name": "Trekking in Kalaw", + "description": "Embark on a scenic trek through the rolling hills and tribal villages surrounding Kalaw. Immerse yourself in the local culture, interact with ethnic minority communities, and witness breathtaking landscapes. Choose from various trekking routes and durations to suit your fitness level and interests.", + "locationName": "Kalaw", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "myanmar", + "ref": "trekking-in-kalaw", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_trekking-in-kalaw.jpg" + }, + { + "name": "Visit Mrauk U", + "description": "Step back in time at Mrauk U, an ancient city known for its remarkable temples and pagodas. Explore the archaeological wonders, wander through the atmospheric ruins, and discover the rich history of this once-powerful kingdom.", + "locationName": "Mrauk U", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "myanmar", + "ref": "visit-mrauk-u", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_visit-mrauk-u.jpg" + }, + { + "name": "Kayaking on Inle Lake", + "description": "Embark on a serene kayaking adventure on Inle Lake, gliding through the tranquil waters surrounded by floating gardens, stilt villages, and local fishermen practicing their unique leg-rowing technique. Witness the breathtaking scenery, observe the daily life of the Intha people, and immerse yourself in the peaceful atmosphere of this iconic lake.", + "locationName": "Inle Lake", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "myanmar", + "ref": "kayaking-on-inle-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_kayaking-on-inle-lake.jpg" + }, + { + "name": "Meditation Retreat in a Buddhist Monastery", + "description": "Find inner peace and tranquility with a meditation retreat at a traditional Buddhist monastery. Learn the art of mindfulness from experienced monks, participate in guided meditation sessions, and immerse yourself in the spiritual atmosphere of these sacred spaces. This experience offers a unique opportunity for self-reflection and personal growth.", + "locationName": "Various monasteries throughout Myanmar", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "myanmar", + "ref": "meditation-retreat-in-a-buddhist-monastery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_meditation-retreat-in-a-buddhist-monastery.jpg" + }, + { + "name": "Hot Air Balloon Ride over Bagan", + "description": "Soar above the ancient temples of Bagan in a hot air balloon, witnessing the breathtaking panoramic views of the archaeological wonder at sunrise. Capture stunning aerial photographs of the pagodas and stupas as the golden light bathes the landscape, creating an unforgettable and magical experience.", + "locationName": "Bagan", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "myanmar", + "ref": "hot-air-balloon-ride-over-bagan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_hot-air-balloon-ride-over-bagan.jpg" + }, + { + "name": "Explore the Pindaya Caves", + "description": "Venture into the mystical Pindaya Caves, a labyrinthine network of caverns housing thousands of Buddha images. Marvel at the intricate carvings and sculptures, learn about the spiritual significance of the site, and experience the unique atmosphere of this hidden gem.", + "locationName": "Pindaya", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "myanmar", + "ref": "explore-the-pindaya-caves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_explore-the-pindaya-caves.jpg" + }, + { + "name": "Cooking Class with a Local Family", + "description": "Delve into the world of Burmese cuisine with a hands-on cooking class hosted by a local family. Learn the secrets of traditional dishes, discover the unique flavors and ingredients of Myanmar, and enjoy a delicious home-cooked meal with your hosts. This experience offers a glimpse into the local culture and culinary traditions.", + "locationName": "Various locations throughout Myanmar", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "myanmar", + "ref": "cooking-class-with-a-local-family", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_cooking-class-with-a-local-family.jpg" + }, + { + "name": "Volunteer at an Elephant Sanctuary", + "description": "Immerse yourself in the ethical treatment of elephants at a sanctuary like Green Hill Valley Elephant Camp. Observe these gentle giants in their natural habitat, learn about their care and conservation efforts, and even assist with feeding and bathing them. This heartwarming experience provides a responsible way to interact with elephants and support their well-being.", + "locationName": "Green Hill Valley Elephant Camp or similar sanctuaries", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "myanmar", + "ref": "volunteer-at-an-elephant-sanctuary", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_volunteer-at-an-elephant-sanctuary.jpg" + }, + { + "name": "Explore the Mergui Archipelago", + "description": "Embark on an island-hopping adventure through the untouched beauty of the Mergui Archipelago. Discover secluded beaches, snorkel in crystal-clear waters teeming with marine life, and kayak along pristine coastlines. This remote paradise offers a unique opportunity to escape the crowds and connect with nature's wonders.", + "locationName": "Mergui Archipelago", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "myanmar", + "ref": "explore-the-mergui-archipelago", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_explore-the-mergui-archipelago.jpg" + }, + { + "name": "Visit a Local Market", + "description": "Delve into the vibrant atmosphere of a local market, such as Bogyoke Aung San Market in Yangon. Browse through stalls overflowing with handcrafted souvenirs, textiles, jewelry, and fresh produce. Engage with friendly vendors, practice your bargaining skills, and discover unique treasures to take home.", + "locationName": "Bogyoke Aung San Market or other local markets", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "myanmar", + "ref": "visit-a-local-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_visit-a-local-market.jpg" + }, + { + "name": "Take a Burmese Cooking Class", + "description": "Learn the art of Burmese cuisine by taking a cooking class. Discover the unique flavors and spices used in traditional dishes, and master the techniques of creating popular recipes like Mohinga (fish noodle soup) or Lahpet Thoke (pickled tea leaf salad). This hands-on experience will tantalize your taste buds and leave you with newfound culinary skills.", + "locationName": "Various cooking schools or local homes", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "myanmar", + "ref": "take-a-burmese-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_take-a-burmese-cooking-class.jpg" + }, + { + "name": "Enjoy a Traditional Puppet Show", + "description": "Experience the magic of Burmese puppetry at a traditional show. Marvel at the intricate marionettes and their graceful movements as they depict ancient legends and folktales. This cultural performance offers a glimpse into Myanmar's rich artistic heritage and storytelling traditions.", + "locationName": "Htwe Oo Myanmar Puppet Theatre or other venues", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "myanmar", + "ref": "enjoy-a-traditional-puppet-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/myanmar_enjoy-a-traditional-puppet-show.jpg" + }, + { + "name": "Soak Up the Sun on Paradise Beach", + "description": "Spend a relaxing day on the golden sands of Paradise Beach, one of Mykonos' most famous and vibrant shores. Enjoy swimming in the crystal-clear turquoise waters, try exciting water sports like jet skiing or parasailing, or simply unwind on a sunbed with a refreshing cocktail from a beachfront bar. The lively atmosphere and stunning scenery make it a perfect beach escape.", + "locationName": "Paradise Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mykonos", + "ref": "soak-up-the-sun-on-paradise-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_soak-up-the-sun-on-paradise-beach.jpg" + }, + { + "name": "Explore the Picturesque Mykonos Town", + "description": "Wander through the charming labyrinthine streets of Mykonos Town, also known as Chora. Admire the iconic whitewashed houses with blue accents, discover hidden boutiques and art galleries, and visit historical landmarks like the windmills and the Paraportiani Church. Enjoy a delightful meal at a traditional taverna and soak up the vibrant atmosphere of this picturesque town.", + "locationName": "Mykonos Town (Chora)", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "mykonos", + "ref": "explore-the-picturesque-mykonos-town", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_explore-the-picturesque-mykonos-town.jpg" + }, + { + "name": "Experience the Legendary Mykonos Nightlife", + "description": "As the sun sets, immerse yourself in Mykonos' electrifying nightlife scene. Dance the night away at world-renowned beach clubs like Paradise Club or Cavo Paradiso, enjoy live music performances at bars and restaurants, or sip cocktails at chic lounge bars overlooking the Aegean Sea. Mykonos offers a diverse and unforgettable nightlife experience for every taste.", + "locationName": "Various locations in Mykonos", + "duration": 5, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "mykonos", + "ref": "experience-the-legendary-mykonos-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_experience-the-legendary-mykonos-nightlife.jpg" + }, + { + "name": "Discover Ancient Delos", + "description": "Embark on a historical journey to the nearby island of Delos, a UNESCO World Heritage Site and one of the most important archaeological sites in Greece. Explore the ancient ruins of temples, houses, and theaters, and learn about the island's rich history and mythology. Delos offers a fascinating glimpse into the past and a unique cultural experience.", + "locationName": "Delos Island", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "mykonos", + "ref": "discover-ancient-delos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_discover-ancient-delos.jpg" + }, + { + "name": "Indulge in Delicious Greek Cuisine", + "description": "Tantalize your taste buds with the authentic flavors of Greek cuisine. Enjoy fresh seafood dishes at waterfront tavernas, savor traditional Greek specialties like moussaka and souvlaki, or indulge in gourmet dining experiences at upscale restaurants. Mykonos offers a wide range of culinary options to satisfy every palate.", + "locationName": "Various restaurants and tavernas in Mykonos", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "mykonos", + "ref": "indulge-in-delicious-greek-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_indulge-in-delicious-greek-cuisine.jpg" + }, + { + "name": "Set Sail on a Catamaran Cruise", + "description": "Embark on a luxurious catamaran cruise around the Aegean Sea, soaking in the breathtaking views of the coastline and nearby islands. Swim and snorkel in crystal-clear waters, bask in the sun on deck, and enjoy a delicious meal prepared on board. This is the perfect way to experience the beauty of Mykonos from a different perspective. #Beach #Island #Sailing #Luxury #Romantic #Family-friendly", + "locationName": "Mykonos Harbor", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "mykonos", + "ref": "set-sail-on-a-catamaran-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_set-sail-on-a-catamaran-cruise.jpg" + }, + { + "name": "Go Windsurfing or Kitesurfing at Kalafatis Beach", + "description": "Feel the adrenaline rush as you harness the power of the wind and glide across the waves. Kalafatis Beach is renowned for its ideal wind conditions, making it a haven for windsurfing and kitesurfing enthusiasts. Whether you're a beginner or an experienced rider, there are lessons and equipment rentals available to ensure an unforgettable experience. #Beach #Adventure #sports #Summer #destination", + "locationName": "Kalafatis Beach", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "mykonos", + "ref": "go-windsurfing-or-kitesurfing-at-kalafatis-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_go-windsurfing-or-kitesurfing-at-kalafatis-beach.jpg" + }, + { + "name": "Hike to the Armenistis Lighthouse", + "description": "Embark on a scenic hike to the historic Armenistis Lighthouse, offering panoramic views of the island and the Aegean Sea. The trail winds through rugged landscapes and charming villages, providing a glimpse into the island's natural beauty and local life. #Hiking #Off-the-beaten-path #Cultural #experiences #Great #for #photography #Instagrammable", + "locationName": "Fanari", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "mykonos", + "ref": "hike-to-the-armenistis-lighthouse", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_hike-to-the-armenistis-lighthouse.jpg" + }, + { + "name": "Visit the Monastery of Panagia Tourliani", + "description": "Step back in time at the Monastery of Panagia Tourliani, a beautiful example of Cycladic architecture dating back to the 16th century. Admire the intricate marble carvings, ornate iconostasis, and peaceful atmosphere of this historic religious site. #Historic #Cultural #experiences #Secluded", + "locationName": "Ano Mera village", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "mykonos", + "ref": "visit-the-monastery-of-panagia-tourliani", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_visit-the-monastery-of-panagia-tourliani.jpg" + }, + { + "name": "Take a Cooking Class and Learn to Make Traditional Greek Dishes", + "description": "Immerse yourself in the flavors of Greece by taking a cooking class and learning to prepare authentic dishes like moussaka, souvlaki, and baklava. Discover the secrets of Greek cuisine from local chefs and enjoy the fruits of your labor with a delicious meal. #Cultural #experiences #Foodie #Family-friendly", + "locationName": "Mykonos Town", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "mykonos", + "ref": "take-a-cooking-class-and-learn-to-make-traditional-greek-dishes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_take-a-cooking-class-and-learn-to-make-traditional-greek-dishes.jpg" + }, + { + "name": "Horseback Riding at Ano Mera", + "description": "Embark on a serene horseback riding adventure through the charming village of Ano Mera and its surrounding countryside. Experience the island's natural beauty from a unique perspective as you trot past traditional houses, olive groves, and hidden chapels. This activity is suitable for all skill levels and offers a peaceful escape from the bustling beaches.", + "locationName": "Ano Mera", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mykonos", + "ref": "horseback-riding-at-ano-mera", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_horseback-riding-at-ano-mera.jpg" + }, + { + "name": "Little Venice Sunset Stroll", + "description": "Capture the enchanting beauty of Little Venice, a picturesque neighborhood known for its colorful houses perched on the edge of the Aegean Sea. As the sun begins its descent, take a leisurely stroll along the waterfront, admiring the golden hues reflecting on the water and the charming architecture. Find a cozy spot to savor a glass of local wine and witness a breathtaking Mykonos sunset.", + "locationName": "Little Venice", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "mykonos", + "ref": "little-venice-sunset-stroll", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_little-venice-sunset-stroll.jpg" + }, + { + "name": "Scuba Diving Adventure", + "description": "Dive into the crystal-clear waters surrounding Mykonos and discover a vibrant underwater world. Explore fascinating reefs, shipwrecks, and underwater caves teeming with marine life. Whether you're a seasoned diver or a beginner, there are numerous diving centers offering guided excursions and courses, allowing you to experience the thrill of exploring the Aegean Sea's hidden depths.", + "locationName": "Various diving centers around Mykonos", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "mykonos", + "ref": "scuba-diving-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_scuba-diving-adventure.jpg" + }, + { + "name": "Mykonos Farmers Market Visit", + "description": "Immerse yourself in the local culture and flavors at the Mykonos Farmers Market. Discover a vibrant array of fresh produce, local cheeses, honey, herbs, and handmade crafts. Engage with friendly vendors, sample delicious treats, and find unique souvenirs to take home. This is a perfect opportunity to experience the authentic side of Mykonos and support local businesses.", + "locationName": "Mykonos Town", + "duration": 1.5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "mykonos", + "ref": "mykonos-farmers-market-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_mykonos-farmers-market-visit.jpg" + }, + { + "name": "Private Yacht Excursion to Rhenia Island", + "description": "Embark on a luxurious private yacht excursion to the secluded island of Rhenia. Escape the crowds and discover pristine beaches, turquoise waters, and unspoiled landscapes. Enjoy swimming, snorkeling, and sunbathing in complete privacy. Some excursions offer gourmet meals and water sports activities for an unforgettable day trip.", + "locationName": "Rhenia Island", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 5, + "destinationRef": "mykonos", + "ref": "private-yacht-excursion-to-rhenia-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_private-yacht-excursion-to-rhenia-island.jpg" + }, + { + "name": "Jeep Safari Adventure", + "description": "Embark on an exhilarating jeep safari tour that takes you off the beaten path to discover Mykonos' hidden gems. Traverse rugged terrains, visit secluded beaches, and witness breathtaking panoramic views of the island. This adventure offers a unique perspective of Mykonos beyond the typical tourist spots.", + "locationName": "Mykonos Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "mykonos", + "ref": "jeep-safari-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_jeep-safari-adventure.jpg" + }, + { + "name": "Wine Tasting at a Local Vineyard", + "description": "Indulge in the flavors of Mykonos with a visit to a local vineyard. Learn about the island's unique grape varieties and winemaking traditions while enjoying a guided tasting of their exquisite wines. Savor the distinct notes and aromas of Mykonos' wines amidst picturesque vineyard landscapes.", + "locationName": "Mykonos Vineyards", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "mykonos", + "ref": "wine-tasting-at-a-local-vineyard", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_wine-tasting-at-a-local-vineyard.jpg" + }, + { + "name": "Kayaking and Snorkeling Tour", + "description": "Explore the crystal-clear waters of Mykonos on a kayaking and snorkeling adventure. Paddle along the coastline, discovering hidden coves and beaches inaccessible by land. Dive beneath the surface to snorkel amidst vibrant marine life and explore the underwater world of the Aegean Sea.", + "locationName": "Mykonos Coastline", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "mykonos", + "ref": "kayaking-and-snorkeling-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_kayaking-and-snorkeling-tour.jpg" + }, + { + "name": "Sunset Yoga Session", + "description": "Find your inner peace and connect with nature during a magical sunset yoga session. Practice yoga poses on a secluded beach or a rooftop terrace overlooking the Aegean Sea as the sun dips below the horizon, painting the sky with vibrant hues. This experience offers a perfect blend of relaxation and rejuvenation.", + "locationName": "Various Locations", + "duration": 1.5, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 2, + "destinationRef": "mykonos", + "ref": "sunset-yoga-session", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_sunset-yoga-session.jpg" + }, + { + "name": "Art and Culture Tour", + "description": "Immerse yourself in the artistic and cultural heritage of Mykonos. Visit local art galleries showcasing works by renowned Greek artists, explore historical churches and archaeological sites, and discover the island's rich history and traditions through guided tours and interactive experiences.", + "locationName": "Mykonos Town and surrounding areas", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "mykonos", + "ref": "art-and-culture-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/mykonos_art-and-culture-tour.jpg" + }, + { + "name": "David Sheldrick Elephant Orphanage", + "description": "Witness the heartwarming efforts of this sanctuary as they care for orphaned baby elephants. Observe feeding time, learn about conservation efforts, and even foster an elephant.", + "locationName": "David Sheldrick Elephant Orphanage", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "nairobi", + "ref": "david-sheldrick-elephant-orphanage", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_david-sheldrick-elephant-orphanage.jpg" + }, + { + "name": "Karen Blixen Museum", + "description": "Step back in time at the former home of the famous author of 'Out of Africa.' Explore the colonial farmhouse, lush gardens, and coffee farm, and delve into Kenya's history.", + "locationName": "Karen Blixen Museum", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "nairobi", + "ref": "karen-blixen-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_karen-blixen-museum.jpg" + }, + { + "name": "Nairobi National Park Safari", + "description": "Embark on a thrilling safari adventure just outside the city limits. Spot iconic African wildlife like lions, rhinos, giraffes, and zebras against the backdrop of the city skyline.", + "locationName": "Nairobi National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "nairobi", + "ref": "nairobi-national-park-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_nairobi-national-park-safari.jpg" + }, + { + "name": "Kazuri Beads Women's Cooperative", + "description": "Visit this inspiring workshop and witness the creation of beautiful handcrafted ceramic beads. Learn about the women's empowerment project and support local artisans by purchasing unique souvenirs.", + "locationName": "Kazuri Beads Women's Cooperative", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "nairobi", + "ref": "kazuri-beads-women-s-cooperative", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_kazuri-beads-women-s-cooperative.jpg" + }, + { + "name": "Carnivore Restaurant Experience", + "description": "Indulge in a unique dining experience at Carnivore, famed for its all-you-can-eat meat feast. Sample exotic meats like ostrich, crocodile, and camel, cooked over a charcoal grill.", + "locationName": "Carnivore Restaurant", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "nairobi", + "ref": "carnivore-restaurant-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_carnivore-restaurant-experience.jpg" + }, + { + "name": "Giraffe Centre", + "description": "Get up close and personal with endangered Rothschild's giraffes at the Giraffe Centre. You can feed them, learn about conservation efforts, and even kiss one if you're feeling adventurous! This is a perfect family-friendly activity that allows you to interact with these gentle giants in a safe and educational environment.", + "locationName": "Giraffe Centre", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "nairobi", + "ref": "giraffe-centre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_giraffe-centre.jpg" + }, + { + "name": "Maasai Market", + "description": "Immerse yourself in the vibrant culture of the Maasai people at the Maasai Market. Browse through stalls overflowing with colorful handcrafted jewelry, textiles, and souvenirs. Bargaining is expected, so hone your negotiation skills and find unique treasures to take home.", + "locationName": "Various locations in Nairobi", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "nairobi", + "ref": "maasai-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_maasai-market.jpg" + }, + { + "name": "Karura Forest", + "description": "Escape the city bustle and explore the serene Karura Forest. Hike or bike through the lush trails, discover hidden waterfalls, and visit the Mau Mau caves, which hold historical significance. This is a perfect escape for nature lovers and those seeking a moment of tranquility.", + "locationName": "Karura Forest", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "nairobi", + "ref": "karura-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_karura-forest.jpg" + }, + { + "name": "Bomas of Kenya", + "description": "Experience the diverse cultural heritage of Kenya at the Bomas of Kenya. Witness traditional dances, music performances, and homesteads representing different ethnic groups. It's an engaging and educational way to learn about the country's rich cultural tapestry.", + "locationName": "Bomas of Kenya", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "nairobi", + "ref": "bomas-of-kenya", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_bomas-of-kenya.jpg" + }, + { + "name": "Nairobi National Museum", + "description": "Delve into the history and art of Kenya at the Nairobi National Museum. Explore exhibits on archaeology, paleontology, and contemporary art. You can also visit the Snake Park within the museum grounds and see a variety of reptiles.", + "locationName": "Nairobi National Museum", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "nairobi", + "ref": "nairobi-national-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_nairobi-national-museum.jpg" + }, + { + "name": "Hike Ngong Hills", + "description": "Embark on a scenic hike up the Ngong Hills, a series of rolling hills offering breathtaking panoramic views of the Great Rift Valley and the Nairobi skyline. Choose from various trails catering to different fitness levels, and enjoy a picnic lunch amidst the serene natural beauty.", + "locationName": "Ngong Hills", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "nairobi", + "ref": "hike-ngong-hills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_hike-ngong-hills.jpg" + }, + { + "name": "Shop at the Maasai Market", + "description": "Immerse yourself in the vibrant atmosphere of the Maasai Market, a rotating market showcasing authentic Kenyan crafts and souvenirs. Find unique handmade jewelry, textiles, wood carvings, and more, while supporting local artisans and experiencing the rich cultural heritage of the Maasai people.", + "locationName": "Maasai Market (various locations)", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "nairobi", + "ref": "shop-at-the-maasai-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_shop-at-the-maasai-market.jpg" + }, + { + "name": "Visit the Kazuri Beads Women's Cooperative", + "description": "Discover the inspiring work of the Kazuri Beads Women's Cooperative, where disadvantaged women create beautiful handcrafted ceramic beads and pottery. Take a tour of the workshop, learn about the process, and purchase unique souvenirs while supporting women's empowerment and economic independence.", + "locationName": "Kazuri Beads Women's Cooperative", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "nairobi", + "ref": "visit-the-kazuri-beads-women-s-cooperative", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_visit-the-kazuri-beads-women-s-cooperative.jpg" + }, + { + "name": "Experience Kenyan Cuisine", + "description": "Embark on a culinary journey through Kenyan cuisine, known for its diverse flavors and influences. Sample traditional dishes like nyama choma (grilled meat), ugali (maize porridge), and sukuma wiki (collard greens), and explore the vibrant street food scene or dine at upscale restaurants offering modern interpretations of Kenyan classics.", + "locationName": "Various restaurants and street food vendors", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "nairobi", + "ref": "experience-kenyan-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_experience-kenyan-cuisine.jpg" + }, + { + "name": "Enjoy Nairobi's Nightlife", + "description": "Experience the vibrant nightlife of Nairobi, with a diverse range of bars, clubs, and live music venues. Dance the night away to Afrobeat rhythms, enjoy live bands playing local and international music, or unwind with a cocktail at a rooftop bar overlooking the city skyline.", + "locationName": "Various bars, clubs, and live music venues", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "nairobi", + "ref": "enjoy-nairobi-s-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_enjoy-nairobi-s-nightlife.jpg" + }, + { + "name": "Hot Air Balloon Safari Over the Maasai Mara", + "description": "Embark on an unforgettable hot air balloon safari over the Maasai Mara National Reserve. Witness the breathtaking sunrise as you soar above the vast savanna, observing wildlife like lions, elephants, and giraffes in their natural habitat. Enjoy a champagne breakfast upon landing, creating a truly magical experience.", + "locationName": "Maasai Mara National Reserve", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "nairobi", + "ref": "hot-air-balloon-safari-over-the-maasai-mara", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_hot-air-balloon-safari-over-the-maasai-mara.jpg" + }, + { + "name": "Visit the Giraffe Manor", + "description": "Experience a unique stay at the Giraffe Manor, a charming boutique hotel known for its resident Rothschild giraffes. Enjoy breakfast or afternoon tea on the terrace as these gentle giants poke their heads through the windows, creating unforgettable memories and photo opportunities.", + "locationName": "Giraffe Manor", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "nairobi", + "ref": "visit-the-giraffe-manor", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_visit-the-giraffe-manor.jpg" + }, + { + "name": "Explore the Ngong Hills", + "description": "Embark on a scenic hike or bike ride through the Ngong Hills, a series of peaks offering panoramic views of the Great Rift Valley and Nairobi National Park. Enjoy the fresh air, diverse birdlife, and stunning landscapes, making it a perfect escape from the city.", + "locationName": "Ngong Hills", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "nairobi", + "ref": "explore-the-ngong-hills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_explore-the-ngong-hills.jpg" + }, + { + "name": "Discover Kenyan Art at the Nairobi Gallery", + "description": "Immerse yourself in Kenyan art and culture at the Nairobi Gallery. Housed in a historic building, the gallery showcases contemporary and traditional art pieces, sculptures, and photography. Learn about the country's rich artistic heritage and support local artists.", + "locationName": "Nairobi Gallery", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "nairobi", + "ref": "discover-kenyan-art-at-the-nairobi-gallery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_discover-kenyan-art-at-the-nairobi-gallery.jpg" + }, + { + "name": "Unwind at the Oloolua Nature Trail", + "description": "Escape the city bustle and reconnect with nature at the Oloolua Nature Trail. Hike through the lush indigenous forest, discover hidden waterfalls, and spot diverse bird species. Enjoy a peaceful picnic by the river, making it a perfect retreat for nature lovers.", + "locationName": "Oloolua Nature Trail", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "nairobi", + "ref": "unwind-at-the-oloolua-nature-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nairobi_unwind-at-the-oloolua-nature-trail.jpg" + }, + { + "name": "Etosha National Park Safari", + "description": "Embark on an unforgettable safari adventure in Etosha National Park, one of Africa's premier wildlife sanctuaries. Witness incredible biodiversity, including elephants, lions, rhinos, giraffes, and countless bird species, as you traverse the vast savannas and gather around watering holes. Capture stunning photographs and create lasting memories in this wildlife paradise.", + "locationName": "Etosha National Park", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "namibia", + "ref": "etosha-national-park-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_etosha-national-park-safari.jpg" + }, + { + "name": "Sossusvlei Dune Climbing and Deadvlei Exploration", + "description": "Challenge yourself with an exhilarating climb up the towering red sand dunes of Sossusvlei, some of the highest in the world. Marvel at the surreal landscapes of Deadvlei, a white clay pan dotted with ancient, skeletal camelthorn trees, creating a photographer's dream. Capture the contrasting colors and unique textures of this otherworldly desert environment.", + "locationName": "Sossusvlei and Deadvlei", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "namibia", + "ref": "sossusvlei-dune-climbing-and-deadvlei-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_sossusvlei-dune-climbing-and-deadvlei-exploration.jpg" + }, + { + "name": "Skeleton Coast Adventure", + "description": "Explore the rugged and remote Skeleton Coast, known for its shipwrecks, seal colonies, and desolate beauty. Embark on a guided tour to discover the history and stories behind the shipwrecks, observe the playful seals basking on the shores, and witness the raw power of the Atlantic Ocean crashing against the coastline.", + "locationName": "Skeleton Coast", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "namibia", + "ref": "skeleton-coast-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_skeleton-coast-adventure.jpg" + }, + { + "name": "Himba Cultural Experience", + "description": "Immerse yourself in the rich culture of the Himba people, a semi-nomadic tribe known for their unique traditions and distinctive appearance. Visit a Himba village and learn about their way of life, customs, and beliefs. Engage with the locals, witness their intricate hairstyles and jewelry, and gain a deeper understanding of their fascinating culture.", + "locationName": "Kaokoland", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "namibia", + "ref": "himba-cultural-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_himba-cultural-experience.jpg" + }, + { + "name": "Stargazing in the Namib Desert", + "description": "Experience the magic of the Namib Desert night sky, renowned for its exceptional clarity and minimal light pollution. Join a stargazing tour or simply lay back under the blanket of stars and marvel at the Milky Way, constellations, and celestial wonders. Learn about astronomy and constellations from expert guides or enjoy the tranquility of the desert night.", + "locationName": "Namib Desert", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "namibia", + "ref": "stargazing-in-the-namib-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_stargazing-in-the-namib-desert.jpg" + }, + { + "name": "Kayaking with Seals in Walvis Bay", + "description": "Embark on a thrilling kayaking adventure in the calm waters of Walvis Bay, where you'll have the opportunity to paddle alongside playful Cape fur seals. Witness these curious creatures up close as they swim, dive, and bask in the sun. This eco-friendly activity allows you to experience the marine life of Namibia in a unique and unforgettable way.", + "locationName": "Walvis Bay", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "namibia", + "ref": "kayaking-with-seals-in-walvis-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_kayaking-with-seals-in-walvis-bay.jpg" + }, + { + "name": "Quad Biking in the Namib Desert", + "description": "Experience the thrill of quad biking across the vast and scenic landscapes of the Namib Desert. Feel the adrenaline rush as you navigate the sandy terrain, surrounded by towering dunes and breathtaking views. This exhilarating activity is perfect for adventure seekers looking for an unforgettable desert experience.", + "locationName": "Namib Desert", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "namibia", + "ref": "quad-biking-in-the-namib-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_quad-biking-in-the-namib-desert.jpg" + }, + { + "name": "Sandboarding and Dune Skiing in Swakopmund", + "description": "Get your adrenaline pumping with sandboarding and dune skiing in the coastal town of Swakopmund. Glide down the steep slopes of the dunes, feeling the rush of wind against your face. Whether you're a beginner or an experienced thrill-seeker, this activity offers an exciting way to experience the unique desert landscape.", + "locationName": "Swakopmund", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "namibia", + "ref": "sandboarding-and-dune-skiing-in-swakopmund", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_sandboarding-and-dune-skiing-in-swakopmund.jpg" + }, + { + "name": "Explore the German Colonial Town of Lüderitz", + "description": "Step back in time with a visit to the charming coastal town of Lüderitz. Explore the well-preserved German colonial architecture, including the iconic Felsenkirche church perched on a rocky outcrop. Discover the town's rich history and enjoy the unique blend of German and Namibian culture.", + "locationName": "Lüderitz", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "namibia", + "ref": "explore-the-german-colonial-town-of-l-deritz", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_explore-the-german-colonial-town-of-l-deritz.jpg" + }, + { + "name": "Visit the Cheetah Conservation Fund", + "description": "Learn about cheetah conservation efforts at the Cheetah Conservation Fund, a research and education center dedicated to protecting these magnificent animals. Take a guided tour to observe cheetahs up close, learn about their behavior and threats they face, and support the organization's mission to ensure their survival in the wild.", + "locationName": "Cheetah Conservation Fund", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "namibia", + "ref": "visit-the-cheetah-conservation-fund", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_visit-the-cheetah-conservation-fund.jpg" + }, + { + "name": "Fish River Canyon Hiking", + "description": "Embark on a multi-day hiking adventure through the second largest canyon in the world, the Fish River Canyon. Hike along the rim, enjoying breathtaking views of the rugged landscape, or descend into the canyon for a challenging and rewarding experience. Camp under the stars and reconnect with nature in this awe-inspiring setting.", + "locationName": "Fish River Canyon", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "namibia", + "ref": "fish-river-canyon-hiking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_fish-river-canyon-hiking.jpg" + }, + { + "name": "Spitzkoppe Climbing and Camping", + "description": "Challenge yourself with rock climbing and bouldering on the granite peaks of Spitzkoppe, also known as the 'Matterhorn of Namibia'. With various routes for all skill levels, it's a paradise for climbers. Camp at the base and enjoy stunning sunsets and starry nights in this unique landscape.", + "locationName": "Spitzkoppe", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "namibia", + "ref": "spitzkoppe-climbing-and-camping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_spitzkoppe-climbing-and-camping.jpg" + }, + { + "name": "Cape Cross Seal Colony Visit", + "description": "Witness one of the largest colonies of Cape fur seals in the world at Cape Cross. Observe these fascinating creatures in their natural habitat, learn about their behavior and conservation efforts, and capture unforgettable photos of this wildlife spectacle.", + "locationName": "Cape Cross", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "namibia", + "ref": "cape-cross-seal-colony-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_cape-cross-seal-colony-visit.jpg" + }, + { + "name": "Twyfelfontein Rock Art Exploration", + "description": "Step back in time and explore the ancient rock engravings at Twyfelfontein, a UNESCO World Heritage Site. Discover thousands of petroglyphs depicting animals, human figures, and abstract designs, offering a glimpse into the lives and beliefs of early hunter-gatherer societies.", + "locationName": "Twyfelfontein", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "namibia", + "ref": "twyfelfontein-rock-art-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_twyfelfontein-rock-art-exploration.jpg" + }, + { + "name": "Kolmanskop Ghost Town Photography", + "description": "Explore the eerie abandoned diamond mining town of Kolmanskop, swallowed by the Namib Desert sands. Capture haunting images of the decaying buildings, once opulent homes and facilities, now reclaimed by nature. This unique location offers a photographer's dream with its contrasting colors and textures.", + "locationName": "Kolmanskop", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "namibia", + "ref": "kolmanskop-ghost-town-photography", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_kolmanskop-ghost-town-photography.jpg" + }, + { + "name": "Hot Air Balloon Safari over the Namib Desert", + "description": "Embark on an unforgettable hot air balloon safari over the mesmerizing Namib Desert. Drift silently above the towering dunes, witnessing the breathtaking landscapes bathed in the golden hues of sunrise. Spot wildlife from a unique perspective as you soar through the clear desert air, creating memories that will last a lifetime.", + "locationName": "Namib Desert", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "namibia", + "ref": "hot-air-balloon-safari-over-the-namib-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_hot-air-balloon-safari-over-the-namib-desert.jpg" + }, + { + "name": "Dolphin and Whale Watching Cruise", + "description": "Set sail on a thrilling marine adventure along the Namibian coast, where you can encounter playful dolphins and majestic whales. Witness these incredible creatures in their natural habitat as they breach, tail slap, and glide through the waves. Learn about marine conservation efforts and enjoy the fresh ocean breeze.", + "locationName": "Walvis Bay", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "namibia", + "ref": "dolphin-and-whale-watching-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_dolphin-and-whale-watching-cruise.jpg" + }, + { + "name": "Township Cultural Tour in Katutura", + "description": "Immerse yourself in the vibrant culture of Namibia's capital city with a township tour in Katutura. Interact with local residents, learn about their daily lives, and discover the rich history and traditions of the community. Visit local markets, sample traditional cuisine, and gain a deeper understanding of Namibian culture.", + "locationName": "Windhoek", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "namibia", + "ref": "township-cultural-tour-in-katutura", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_township-cultural-tour-in-katutura.jpg" + }, + { + "name": "Scenic Flight over the Skeleton Coast", + "description": "Take to the skies for a breathtaking scenic flight over the rugged Skeleton Coast. Marvel at the dramatic coastline, shipwrecks scattered along the shore, and vast seal colonies. Capture stunning aerial photographs of this unique and desolate landscape.", + "locationName": "Skeleton Coast", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "namibia", + "ref": "scenic-flight-over-the-skeleton-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_scenic-flight-over-the-skeleton-coast.jpg" + }, + { + "name": "Living Desert Tour", + "description": "Embark on a fascinating journey into the heart of the Namib Desert with a Living Desert Tour. Learn about the unique adaptations of desert flora and fauna, discover hidden creatures such as the dancing white lady spider and the palmato gecko, and gain a deeper appreciation for the delicate desert ecosystem.", + "locationName": "Namib Desert", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "namibia", + "ref": "living-desert-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/namibia_living-desert-tour.jpg" + }, + { + "name": "Wine Tasting Tour", + "description": "Embark on a delightful journey through Napa Valley's renowned wineries. Visit charming vineyards, learn about the winemaking process, and indulge in tastings of exquisite Cabernet Sauvignon, Chardonnay, and other varietals. Knowledgeable guides will share insights into the region's history and terroir, making it an unforgettable experience for wine enthusiasts.", + "locationName": "Various wineries throughout Napa Valley", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "napa-valley", + "ref": "wine-tasting-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_wine-tasting-tour.jpg" + }, + { + "name": "Gourmet Food Tour", + "description": "Tantalize your taste buds with a culinary adventure through Napa Valley's acclaimed restaurants. Sample delectable dishes prepared by renowned chefs, paired with local wines. Explore the vibrant culinary scene, from farm-to-table experiences to Michelin-starred establishments, and discover the region's gastronomic delights.", + "locationName": "Various restaurants in Napa Valley", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "napa-valley", + "ref": "gourmet-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_gourmet-food-tour.jpg" + }, + { + "name": "Hot Air Balloon Ride", + "description": "Soar above the picturesque vineyards of Napa Valley in a hot air balloon. Witness breathtaking panoramic views of rolling hills, sprawling estates, and the distant Mayacamas Mountains. Enjoy a serene and unforgettable experience as you float gently through the air, capturing stunning photos and creating lasting memories.", + "locationName": "Napa Valley Aloft or Balloons Above the Valley", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "napa-valley", + "ref": "hot-air-balloon-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_hot-air-balloon-ride.jpg" + }, + { + "name": "Napa Valley Wine Train", + "description": "Embark on a scenic and luxurious journey aboard the Napa Valley Wine Train. Travel through the heart of wine country in vintage Pullman cars, enjoying gourmet meals, fine wines, and breathtaking views of the vineyards. Choose from various themed tours, including murder mystery dinners and wine tasting experiences, for an unforgettable adventure.", + "locationName": "Napa Valley Wine Train", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "napa-valley", + "ref": "napa-valley-wine-train", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_napa-valley-wine-train.jpg" + }, + { + "name": "Spa Day and Wellness Retreat", + "description": "Indulge in a day of pampering and relaxation at one of Napa Valley's luxurious spa resorts. Enjoy rejuvenating treatments, such as massages, facials, and body wraps, amidst tranquil surroundings. Many resorts offer wellness programs, yoga classes, and fitness activities, allowing you to unwind and recharge in a serene environment.", + "locationName": "Various spa resorts in Napa Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "napa-valley", + "ref": "spa-day-and-wellness-retreat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_spa-day-and-wellness-retreat.jpg" + }, + { + "name": "Napa Valley Bike Tour", + "description": "Embark on a scenic bike tour through the rolling vineyards of Napa Valley. Pedal along quiet country roads, stopping at charming wineries for tastings and breathtaking views. Enjoy a picnic lunch amidst the vines, soaking up the fresh air and sunshine. This is a fantastic way to experience the beauty of the region at your own pace.", + "locationName": "Various vineyards and wineries throughout Napa Valley", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "napa-valley", + "ref": "napa-valley-bike-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_napa-valley-bike-tour.jpg" + }, + { + "name": "Culinary Class at the Culinary Institute of America", + "description": "Unleash your inner chef with a hands-on cooking class at the renowned Culinary Institute of America (CIA) at Greystone. Learn from world-class chefs and master new culinary techniques, creating delicious dishes using fresh, local ingredients. This immersive experience is perfect for food enthusiasts of all skill levels.", + "locationName": "Culinary Institute of America at Greystone", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "napa-valley", + "ref": "culinary-class-at-the-culinary-institute-of-america", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_culinary-class-at-the-culinary-institute-of-america.jpg" + }, + { + "name": "Kayaking on the Napa River", + "description": "Explore the tranquil beauty of the Napa River on a leisurely kayak adventure. Paddle through serene waters, surrounded by lush vineyards and rolling hills. Keep an eye out for wildlife like herons and egrets as you enjoy the peaceful ambiance of the valley from a unique perspective.", + "locationName": "Napa River", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "napa-valley", + "ref": "kayaking-on-the-napa-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_kayaking-on-the-napa-river.jpg" + }, + { + "name": "Uptown Theater Performance", + "description": "Experience the vibrant arts and culture scene of Napa Valley with a live performance at the historic Uptown Theater. Enjoy a variety of shows, from Broadway musicals and concerts to comedy acts and dance performances. This is a perfect evening activity for those seeking entertainment and a touch of sophistication.", + "locationName": "Uptown Theater, Napa", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "napa-valley", + "ref": "uptown-theater-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_uptown-theater-performance.jpg" + }, + { + "name": "Oxbow Public Market Exploration", + "description": "Indulge in a culinary adventure at the Oxbow Public Market, a vibrant marketplace showcasing the best of Napa Valley's artisanal food and wine. Sample local cheeses, charcuterie, fresh produce, and handcrafted chocolates. Discover unique gifts and souvenirs, or simply soak up the lively atmosphere of this foodie haven.", + "locationName": "Oxbow Public Market, Napa", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "napa-valley", + "ref": "oxbow-public-market-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_oxbow-public-market-exploration.jpg" + }, + { + "name": "Hiking Among the Vineyards", + "description": "Embark on a scenic hike through the rolling hills of Napa Valley, surrounded by picturesque vineyards. Trails like the Oat Hill Mine Trail offer stunning views and a chance to immerse yourself in the region's natural beauty.", + "locationName": "Oat Hill Mine Trail, Robert Louis Stevenson State Park", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "napa-valley", + "ref": "hiking-among-the-vineyards", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_hiking-among-the-vineyards.jpg" + }, + { + "name": "Napa Valley Wine Trolley", + "description": "Step back in time and explore Napa Valley in a replica of an iconic San Francisco cable car. The Napa Valley Wine Trolley offers a unique and charming way to visit different wineries, enjoying the scenery and learning about the region's history.", + "locationName": "Napa Valley Wine Trolley", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "napa-valley", + "ref": "napa-valley-wine-trolley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_napa-valley-wine-trolley.jpg" + }, + { + "name": "Castello di Amorosa", + "description": "Transport yourself to a medieval Tuscan castle at Castello di Amorosa. Explore the authentically-built 13th-century style castle, complete with a moat, drawbridge, and torture chamber. Enjoy a tour and wine tasting in this unique setting.", + "locationName": "Castello di Amorosa", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "napa-valley", + "ref": "castello-di-amorosa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_castello-di-amorosa.jpg" + }, + { + "name": "Beringer Vineyards Tour", + "description": "Delve into the history of California winemaking with a tour of Beringer Vineyards, the oldest continuously operating winery in Napa Valley. Explore the historic Rhine House, discover the aging caves, and indulge in a tasting of their renowned wines.", + "locationName": "Beringer Vineyards", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "napa-valley", + "ref": "beringer-vineyards-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_beringer-vineyards-tour.jpg" + }, + { + "name": "Hot Air Balloon Ride Over the Valley", + "description": "Soar above the breathtaking landscapes of Napa Valley in a hot air balloon. Witness the sunrise or sunset paint the vineyards in golden hues, and enjoy panoramic views of the entire region.", + "locationName": "Napa Valley Balloons, Inc.", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "napa-valley", + "ref": "hot-air-balloon-ride-over-the-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_hot-air-balloon-ride-over-the-valley.jpg" + }, + { + "name": "Napa Valley Aloft Balloon Rides", + "description": "Experience the breathtaking beauty of Napa Valley from a unique perspective with a hot air balloon ride. Drift silently over rolling vineyards, picturesque towns, and the stunning Napa River as the sun paints the sky with vibrant colors. This unforgettable adventure offers panoramic views and a sense of tranquility, making it a perfect romantic or special occasion activity.", + "locationName": "Napa Valley", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "napa-valley", + "ref": "napa-valley-aloft-balloon-rides", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_napa-valley-aloft-balloon-rides.jpg" + }, + { + "name": "Silverado Trail Scenic Drive", + "description": "Embark on a scenic drive along the historic Silverado Trail, a winding road that traverses the eastern side of Napa Valley. Discover hidden wineries, charming towns, and breathtaking vistas of vineyards and mountains. Stop at local farms and artisan shops along the way, or simply enjoy the peaceful drive through the countryside. This self-guided tour allows you to explore at your own pace and discover the hidden gems of Napa Valley.", + "locationName": "Silverado Trail", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "napa-valley", + "ref": "silverado-trail-scenic-drive", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_silverado-trail-scenic-drive.jpg" + }, + { + "name": "Bothe-Napa Valley State Park", + "description": "Escape the hustle and bustle of wine country and immerse yourself in nature at Bothe-Napa Valley State Park. Hike through redwood forests, explore scenic trails, or have a picnic by a babbling creek. The park offers a range of activities, including camping, swimming, and fishing, making it a perfect destination for outdoor enthusiasts and families.", + "locationName": "Bothe-Napa Valley State Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "napa-valley", + "ref": "bothe-napa-valley-state-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_bothe-napa-valley-state-park.jpg" + }, + { + "name": "Napa Valley Museum Yountville", + "description": "Delve into the rich history and culture of Napa Valley at the Napa Valley Museum Yountville. Explore exhibits showcasing the region's art, winemaking heritage, and the stories of the people who have shaped the valley. The museum offers a fascinating glimpse into the past and present of this iconic destination.", + "locationName": "Yountville", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "napa-valley", + "ref": "napa-valley-museum-yountville", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_napa-valley-museum-yountville.jpg" + }, + { + "name": "Live Music at Blue Note Napa", + "description": "Experience the vibrant nightlife of Napa Valley at the Blue Note Napa, a renowned jazz club. Enjoy live performances by world-class musicians while sipping on your favorite wine or cocktail. The intimate setting and exceptional acoustics create an unforgettable evening of entertainment.", + "locationName": "Napa", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "napa-valley", + "ref": "live-music-at-blue-note-napa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/napa-valley_live-music-at-blue-note-napa.jpg" + }, + { + "name": "Explore the French Quarter", + "description": "Wander through the charming streets of the French Quarter, the oldest neighborhood in New Orleans. Admire the French and Spanish colonial architecture, visit Jackson Square and St. Louis Cathedral, browse the shops and art galleries, and soak in the lively atmosphere.", + "locationName": "French Quarter", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-orleans", + "ref": "explore-the-french-quarter", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_explore-the-french-quarter.jpg" + }, + { + "name": "Indulge in a Culinary Adventure", + "description": "Embark on a delicious journey through New Orleans' renowned cuisine. Savor classic dishes like gumbo, jambalaya, and beignets. Explore local restaurants, cafes, and food markets to experience the unique flavors and spices of Creole and Cajun cooking.", + "locationName": "Various restaurants and cafes", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "new-orleans", + "ref": "indulge-in-a-culinary-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_indulge-in-a-culinary-adventure.jpg" + }, + { + "name": "Immerse in Jazz Music on Frenchmen Street", + "description": "Head to Frenchmen Street, a vibrant hub of live music venues. Experience the soulful sounds of New Orleans jazz, blues, and other genres. Enjoy the lively atmosphere, bar hop, and dance the night away.", + "locationName": "Frenchmen Street", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "new-orleans", + "ref": "immerse-in-jazz-music-on-frenchmen-street", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_immerse-in-jazz-music-on-frenchmen-street.jpg" + }, + { + "name": "Visit the Historic Cemeteries", + "description": "Explore the unique above-ground cemeteries of New Orleans, such as St. Louis Cemetery No. 1. Learn about the city's history, burial customs, and famous figures who are laid to rest there. Take a guided tour or wander through the hauntingly beautiful tombs.", + "locationName": "St. Louis Cemetery No. 1", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-orleans", + "ref": "visit-the-historic-cemeteries", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_visit-the-historic-cemeteries.jpg" + }, + { + "name": "Experience Mardi Gras", + "description": "If you're visiting during Mardi Gras season, immerse yourself in the festivities. Watch colorful parades, catch beads, and join the lively celebrations. Experience the unique traditions and costumes of this world-famous carnival.", + "locationName": "Various parade routes", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "new-orleans", + "ref": "experience-mardi-gras", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_experience-mardi-gras.jpg" + }, + { + "name": "Swamp Tour Adventure", + "description": "Embark on a thrilling swamp tour adventure just outside the city. Glide through the mysterious bayous on an airboat, encountering fascinating wildlife like alligators, turtles, and various bird species. Learn about the unique ecosystem and Cajun culture from experienced guides, making it an unforgettable experience for nature enthusiasts and adventure seekers.", + "locationName": "Various swamp tour operators outside New Orleans", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "new-orleans", + "ref": "swamp-tour-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_swamp-tour-adventure.jpg" + }, + { + "name": "St. Charles Streetcar Ride", + "description": "Take a nostalgic ride on the iconic St. Charles Streetcar, one of the oldest continuously operating streetcar lines in the world. Enjoy a scenic journey through the Garden District, admiring the historic mansions, oak-lined streets, and charming shops. It's a relaxing and affordable way to experience the city's beauty and history.", + "locationName": "St. Charles Avenue", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "new-orleans", + "ref": "st-charles-streetcar-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_st-charles-streetcar-ride.jpg" + }, + { + "name": "Explore the Garden District", + "description": "Wander through the picturesque Garden District, known for its elegant mansions, lush gardens, and historic architecture. Admire the antebellum homes, visit Lafayette Cemetery No. 1, and stroll along the charming streets. Discover hidden gems, antique shops, and local cafes for a delightful afternoon.", + "locationName": "Garden District", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-orleans", + "ref": "explore-the-garden-district", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_explore-the-garden-district.jpg" + }, + { + "name": "Catch a Show at Preservation Hall", + "description": "Experience the magic of traditional New Orleans jazz at Preservation Hall, a historic venue dedicated to preserving the city's musical heritage. Enjoy intimate performances by renowned jazz musicians in a cozy and authentic setting. It's a must-do for music lovers and a chance to immerse yourself in the soul of New Orleans.", + "locationName": "Preservation Hall", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "new-orleans", + "ref": "catch-a-show-at-preservation-hall", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_catch-a-show-at-preservation-hall.jpg" + }, + { + "name": "Visit the National WWII Museum", + "description": "Immerse yourself in history at the National WWII Museum, one of the top-rated museums in the country. Explore interactive exhibits, personal stories, and artifacts that showcase the American experience during World War II. Gain a deeper understanding of the war's impact and the sacrifices made by those who served.", + "locationName": "National WWII Museum", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "new-orleans", + "ref": "visit-the-national-wwii-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_visit-the-national-wwii-museum.jpg" + }, + { + "name": "Kayaking on Bayou St. John", + "description": "Escape the city bustle and embark on a serene kayaking adventure on Bayou St. John. Paddle through the calm waters, surrounded by lush greenery and charming historic homes. Keep an eye out for local wildlife like turtles, birds, and maybe even an alligator. This relaxing activity is perfect for nature lovers and offers a unique perspective of the city.", + "locationName": "Bayou St. John", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-orleans", + "ref": "kayaking-on-bayou-st-john", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_kayaking-on-bayou-st-john.jpg" + }, + { + "name": "Art Exploration in the Warehouse District", + "description": "Immerse yourself in New Orleans' vibrant art scene with a visit to the Warehouse District. Explore contemporary art galleries, studios, and museums, showcasing works by local and international artists. Discover unique pieces, attend art openings, and perhaps even meet some of the artists. This activity is perfect for art enthusiasts and offers a glimpse into the city's creative spirit.", + "locationName": "Warehouse District", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-orleans", + "ref": "art-exploration-in-the-warehouse-district", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_art-exploration-in-the-warehouse-district.jpg" + }, + { + "name": "Shopping Spree at the French Market", + "description": "Indulge in a shopping spree at the historic French Market, a vibrant open-air market dating back to the 18th century. Browse through a diverse array of stalls selling local crafts, souvenirs, antiques, clothing, and fresh produce. Enjoy live music, street performers, and delicious food options. This is a perfect place to find unique gifts and experience the local culture.", + "locationName": "French Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "new-orleans", + "ref": "shopping-spree-at-the-french-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_shopping-spree-at-the-french-market.jpg" + }, + { + "name": "Cocktail Tour and History", + "description": "Embark on a captivating cocktail tour that blends history and mixology. Learn about the origins and evolution of iconic New Orleans cocktails like the Sazerac and the Hurricane. Visit historic bars, hidden speakeasies, and modern craft cocktail lounges. Sip on expertly crafted drinks while discovering the stories and legends behind them. This is a perfect activity for cocktail enthusiasts and history buffs.", + "locationName": "French Quarter and surrounding areas", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "new-orleans", + "ref": "cocktail-tour-and-history", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_cocktail-tour-and-history.jpg" + }, + { + "name": "Day Trip to Oak Alley Plantation", + "description": "Step back in time with a day trip to Oak Alley Plantation, a historic sugar plantation renowned for its stunning avenue of live oak trees. Explore the beautifully preserved mansion, learn about the history of the plantation and the lives of enslaved people, and wander through the picturesque grounds. This is a captivating experience that offers a glimpse into Louisiana's complex past.", + "locationName": "Oak Alley Plantation (Vacherie, LA)", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "new-orleans", + "ref": "day-trip-to-oak-alley-plantation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_day-trip-to-oak-alley-plantation.jpg" + }, + { + "name": "Audubon Park Exploration", + "description": "Escape the city bustle and discover the serene beauty of Audubon Park. Rent a bike or stroll along the picturesque trails, have a picnic under the sprawling oak trees, or visit the Audubon Zoo to encounter exotic animals. Perfect for a relaxing afternoon amidst nature.", + "locationName": "Audubon Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-orleans", + "ref": "audubon-park-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_audubon-park-exploration.jpg" + }, + { + "name": "Steamboat Natchez Jazz Cruise", + "description": "Embark on a nostalgic journey aboard the Steamboat Natchez, a historic steamboat offering scenic cruises along the Mississippi River. Enjoy live jazz music, indulge in a delicious buffet, and admire breathtaking views of the city skyline. A perfect way to experience New Orleans' charm from a different perspective.", + "locationName": "Mississippi River", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "new-orleans", + "ref": "steamboat-natchez-jazz-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_steamboat-natchez-jazz-cruise.jpg" + }, + { + "name": "Jackson Square and St. Louis Cathedral", + "description": "Step into the heart of the French Quarter and visit the iconic Jackson Square, a historic park surrounded by charming buildings and street performers. Explore the majestic St. Louis Cathedral, the oldest continuously active Roman Catholic cathedral in the United States, and admire its stunning architecture.", + "locationName": "Jackson Square", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "new-orleans", + "ref": "jackson-square-and-st-louis-cathedral", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_jackson-square-and-st-louis-cathedral.jpg" + }, + { + "name": "Magazine Street Shopping and Gallery Hopping", + "description": "Wander down the trendy Magazine Street and discover its eclectic mix of boutiques, art galleries, antique shops, and local restaurants. Find unique souvenirs, admire local artwork, or simply soak up the vibrant atmosphere of this popular shopping district.", + "locationName": "Magazine Street", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-orleans", + "ref": "magazine-street-shopping-and-gallery-hopping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_magazine-street-shopping-and-gallery-hopping.jpg" + }, + { + "name": "Catch a Performance at Tipitina's", + "description": "Immerse yourself in the local music scene at Tipitina's, a legendary music venue known for hosting renowned musicians and showcasing the best of New Orleans' musical talent. Enjoy live performances ranging from jazz and blues to funk and rock, and experience the city's vibrant nightlife.", + "locationName": "Tipitina's", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "new-orleans", + "ref": "catch-a-performance-at-tipitina-s", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-orleans_catch-a-performance-at-tipitina-s.jpg" + }, + { + "name": "Hiking the Tongariro Alpine Crossing", + "description": "Embark on a challenging yet rewarding journey through volcanic landscapes on the Tongariro Alpine Crossing. Marvel at emerald lakes, volcanic craters, and panoramic views of the North Island.", + "locationName": "Tongariro National Park", + "duration": 7, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "new-zealand", + "ref": "hiking-the-tongariro-alpine-crossing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_hiking-the-tongariro-alpine-crossing.jpg" + }, + { + "name": "Exploring Waitomo Caves", + "description": "Descend into the magical world of Waitomo Caves and witness the mesmerizing glowworms illuminating the darkness. Take a boat ride through the Glowworm Grotto or opt for a thrilling black water rafting adventure.", + "locationName": "Waitomo", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-zealand", + "ref": "exploring-waitomo-caves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_exploring-waitomo-caves.jpg" + }, + { + "name": "Cruising Milford Sound", + "description": "Experience the awe-inspiring beauty of Milford Sound on a scenic cruise. Gaze upon towering waterfalls, dramatic cliffs, and lush rainforests as you navigate through this fiord carved by glaciers.", + "locationName": "Fiordland National Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "new-zealand", + "ref": "cruising-milford-sound", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_cruising-milford-sound.jpg" + }, + { + "name": "Whale Watching in Kaikoura", + "description": "Embark on an unforgettable whale watching tour in Kaikoura, renowned for its diverse marine life. Spot majestic sperm whales, playful dolphins, and graceful albatrosses.", + "locationName": "Kaikoura", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "new-zealand", + "ref": "whale-watching-in-kaikoura", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_whale-watching-in-kaikoura.jpg" + }, + { + "name": "Bungy Jumping in Queenstown", + "description": "Get your adrenaline pumping with a thrilling bungy jump in the adventure capital of Queenstown. Leap from iconic bridges or platforms and experience the ultimate rush.", + "locationName": "Queenstown", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "new-zealand", + "ref": "bungy-jumping-in-queenstown", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_bungy-jumping-in-queenstown.jpg" + }, + { + "name": "Kayaking in Abel Tasman National Park", + "description": "Embark on a kayaking adventure through the turquoise waters of Abel Tasman National Park. Paddle along the coastline, exploring hidden coves, secluded beaches, and encountering marine life such as seals and dolphins. This activity offers a unique perspective of the park's stunning scenery and is suitable for various skill levels.", + "locationName": "Abel Tasman National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "new-zealand", + "ref": "kayaking-in-abel-tasman-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_kayaking-in-abel-tasman-national-park.jpg" + }, + { + "name": "Stargazing in the Mackenzie Basin", + "description": "Experience the magic of the night sky in the Mackenzie Basin, renowned as one of the world's best stargazing destinations. The region's clear skies and minimal light pollution offer breathtaking views of the Milky Way, constellations, and even the Southern Lights. Join a guided tour or simply lie back and marvel at the celestial wonders above.", + "locationName": "Mackenzie Basin", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-zealand", + "ref": "stargazing-in-the-mackenzie-basin", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_stargazing-in-the-mackenzie-basin.jpg" + }, + { + "name": "Wine Tasting in Marlborough", + "description": "Indulge in the world-renowned wines of Marlborough, New Zealand's largest wine region. Visit picturesque vineyards, sample a variety of Sauvignon Blanc, Pinot Noir, and other varietals, and learn about the winemaking process from passionate local experts. Enjoy a gourmet lunch or cheese platter while savoring the flavors of the region.", + "locationName": "Marlborough", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "new-zealand", + "ref": "wine-tasting-in-marlborough", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_wine-tasting-in-marlborough.jpg" + }, + { + "name": "Rotorua Geothermal Parks", + "description": "Explore the geothermal wonders of Rotorua, a region famous for its geysers, mud pools, and hot springs. Witness the impressive Pohutu Geyser erupt, marvel at the colorful Champagne Pool, and experience the therapeutic benefits of a natural hot spring soak. Learn about the Maori culture and the geothermal forces that shaped the landscape.", + "locationName": "Rotorua", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-zealand", + "ref": "rotorua-geothermal-parks", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_rotorua-geothermal-parks.jpg" + }, + { + "name": "Skiing or Snowboarding in Queenstown", + "description": "Hit the slopes of Queenstown, the adventure capital of New Zealand, and experience world-class skiing and snowboarding. The region boasts several ski resorts with diverse terrain for all skill levels, from gentle beginner slopes to challenging black runs. Enjoy breathtaking mountain views and après-ski activities in the vibrant town center.", + "locationName": "Queenstown", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "new-zealand", + "ref": "skiing-or-snowboarding-in-queenstown", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_skiing-or-snowboarding-in-queenstown.jpg" + }, + { + "name": "Black Water Rafting in Waitomo Caves", + "description": "Embark on a thrilling underground adventure with black water rafting in the Waitomo Caves. Float on an inner tube through the glowworm-lit caverns, rappel down waterfalls, and experience the unique beauty of this subterranean world. This is a perfect activity for those seeking an adrenaline rush and a memorable experience.", + "locationName": "Waitomo Caves", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "new-zealand", + "ref": "black-water-rafting-in-waitomo-caves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_black-water-rafting-in-waitomo-caves.jpg" + }, + { + "name": "Visit Hobbiton Movie Set", + "description": "Step into the enchanting world of Middle-earth with a visit to the Hobbiton Movie Set. Explore the charming hobbit holes, rolling green hills, and gardens of the Shire, as seen in the Lord of the Rings and The Hobbit films. This is a must-do for movie buffs and anyone who wants to experience a bit of magic.", + "locationName": "Matamata", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "new-zealand", + "ref": "visit-hobbiton-movie-set", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_visit-hobbiton-movie-set.jpg" + }, + { + "name": "Relax in the Polynesian Spa", + "description": "Indulge in a rejuvenating experience at the Polynesian Spa in Rotorua. Soak in the mineral-rich geothermal waters, enjoy a relaxing massage, and admire the stunning lakefront views. This is the perfect way to unwind and recharge after a day of exploring.", + "locationName": "Rotorua", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "new-zealand", + "ref": "relax-in-the-polynesian-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_relax-in-the-polynesian-spa.jpg" + }, + { + "name": "Take a Scenic Flight over Franz Josef Glacier", + "description": "Experience the breathtaking beauty of Franz Josef Glacier from above with a scenic helicopter flight. Soar over the ice formations, crevasses, and snow-capped peaks, and enjoy panoramic views of the Southern Alps. This is an unforgettable way to appreciate the scale and majesty of this natural wonder.", + "locationName": "Franz Josef Glacier", + "duration": 1, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "new-zealand", + "ref": "take-a-scenic-flight-over-franz-josef-glacier", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_take-a-scenic-flight-over-franz-josef-glacier.jpg" + }, + { + "name": "Learn About Maori Culture at the Te Papa Museum", + "description": "Immerse yourself in the rich culture and history of the Maori people at the Te Papa Museum in Wellington. Explore interactive exhibits, traditional artifacts, and contemporary art, and gain a deeper understanding of New Zealand's indigenous heritage. This is an enriching and educational experience for visitors of all ages.", + "locationName": "Wellington", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-zealand", + "ref": "learn-about-maori-culture-at-the-te-papa-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_learn-about-maori-culture-at-the-te-papa-museum.jpg" + }, + { + "name": "Caving Adventures in the Waitomo Region", + "description": "Embark on a thrilling caving expedition in the Waitomo region, renowned for its extensive network of limestone caves. Experience the mesmerizing glowworm caves, where thousands of tiny bioluminescent creatures illuminate the darkness. Alternatively, choose a more adventurous route with blackwater rafting, rappelling, or spelunking through hidden passages.", + "locationName": "Waitomo Caves", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "new-zealand", + "ref": "caving-adventures-in-the-waitomo-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_caving-adventures-in-the-waitomo-region.jpg" + }, + { + "name": "Dolphin and Seal Encounters in Kaikoura", + "description": "Kaikoura is a coastal town famous for its marine wildlife. Embark on a boat tour to witness playful dolphins leaping alongside the vessel, or observe the adorable fur seals basking on the rocky shores. You might even spot migrating whales, depending on the season. This experience offers a unique opportunity to connect with nature and appreciate the rich biodiversity of New Zealand's oceans.", + "locationName": "Kaikoura", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "new-zealand", + "ref": "dolphin-and-seal-encounters-in-kaikoura", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_dolphin-and-seal-encounters-in-kaikoura.jpg" + }, + { + "name": "Scenic Drives on the South Island", + "description": "Embark on a breathtaking road trip through the South Island, renowned for its dramatic landscapes. Cruise along the Great Coast Road, winding past rugged coastlines and charming seaside towns. Explore the picturesque Southern Scenic Route, traversing rolling hills, pristine lakes, and snow-capped mountains. Don't miss the Crown Range Road, offering panoramic views of the Queenstown region.", + "locationName": "South Island", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "new-zealand", + "ref": "scenic-drives-on-the-south-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_scenic-drives-on-the-south-island.jpg" + }, + { + "name": "Mountain Biking in the Redwoods", + "description": "Experience the thrill of mountain biking amidst the towering redwoods of Whakarewarewa Forest in Rotorua. Explore the extensive network of trails, catering to all skill levels. Glide through the lush forest, enjoying the fresh air and the unique atmosphere created by these ancient giants.", + "locationName": "Whakarewarewa Forest, Rotorua", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "new-zealand", + "ref": "mountain-biking-in-the-redwoods", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_mountain-biking-in-the-redwoods.jpg" + }, + { + "name": "Skydiving over Lake Taupo", + "description": "Take the plunge and experience the ultimate adrenaline rush with a skydiving adventure over the stunning Lake Taupo. Enjoy breathtaking panoramic views of the lake, volcanic landscapes, and the surrounding mountains as you freefall through the sky. This unforgettable experience is perfect for thrill-seekers and those looking for an unforgettable way to appreciate New Zealand's beauty.", + "locationName": "Lake Taupo", + "duration": 1, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "new-zealand", + "ref": "skydiving-over-lake-taupo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/new-zealand_skydiving-over-lake-taupo.jpg" + }, + { + "name": "Hike the East Coast Trail", + "description": "Embark on an unforgettable journey along the East Coast Trail, a network of coastal paths showcasing Newfoundland's dramatic scenery. Hike through rugged cliffs, secluded coves, and charming fishing villages, encountering breathtaking ocean views and diverse wildlife along the way. Choose from various trail sections, catering to different fitness levels and time constraints, and immerse yourself in the island's natural beauty.", + "locationName": "East Coast Trail", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "hike-the-east-coast-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_hike-the-east-coast-trail.jpg" + }, + { + "name": "Whale Watching Adventure", + "description": "Set sail on a thrilling whale watching excursion and witness the majestic giants of the sea in their natural habitat. Newfoundland's waters are teeming with humpback whales, minke whales, and even orcas. Experienced guides will lead you to prime viewing spots, providing insights into these fascinating creatures and the marine ecosystem. Capture unforgettable memories as you observe whales breaching, tail slapping, and socializing.", + "locationName": "Witless Bay Ecological Reserve", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "newfoundland", + "ref": "whale-watching-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_whale-watching-adventure.jpg" + }, + { + "name": "Explore Gros Morne National Park", + "description": "Discover the awe-inspiring landscapes of Gros Morne National Park, a UNESCO World Heritage Site. Hike through the towering Long Range Mountains, marvel at the majestic fjords of Western Brook Pond, and explore the unique Tablelands, a geological wonder showcasing the Earth's mantle. Engage in boat tours, kayaking adventures, or scenic drives, and immerse yourself in the park's rich natural and cultural heritage.", + "locationName": "Gros Morne National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "explore-gros-morne-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_explore-gros-morne-national-park.jpg" + }, + { + "name": "Experience the Culture of St. John's", + "description": "Immerse yourself in the vibrant culture of St. John's, Newfoundland's capital city. Explore the colorful houses of Jellybean Row, visit the historic Signal Hill National Historic Site, and delve into the exhibits at The Rooms, a cultural center showcasing the province's history and art. Enjoy live music at local pubs, savor fresh seafood delicacies, and experience the warmth and hospitality of the locals.", + "locationName": "St. John's", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "experience-the-culture-of-st-john-s", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_experience-the-culture-of-st-john-s.jpg" + }, + { + "name": "Puffin and Seabird Watching", + "description": "Embark on a delightful boat tour to witness the charming puffins and diverse seabird colonies that inhabit Newfoundland's coastal cliffs. Observe these adorable creatures up close as they nest, socialize, and soar through the air. Learn about their unique behaviors and the importance of conservation efforts, while enjoying the stunning coastal scenery and the refreshing sea breeze.", + "locationName": "Elliston, Bonavista Peninsula", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "puffin-and-seabird-watching", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_puffin-and-seabird-watching.jpg" + }, + { + "name": "Kayaking Adventure in Witless Bay Ecological Reserve", + "description": "Embark on a guided kayaking tour through the pristine waters of Witless Bay Ecological Reserve. Witness breathtaking coastal scenery, encounter playful puffins and other seabirds, and possibly spot whales or dolphins. This eco-friendly activity allows you to connect with nature and experience the island's marine life up close.", + "locationName": "Witless Bay Ecological Reserve", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "newfoundland", + "ref": "kayaking-adventure-in-witless-bay-ecological-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_kayaking-adventure-in-witless-bay-ecological-reserve.jpg" + }, + { + "name": "Scuba Diving at Bell Island", + "description": "Dive into history and explore the underwater world surrounding Bell Island. Discover shipwrecks from World War II, including the SS Lord Strathcona and the SS Saganaga, and marvel at the diverse marine life that inhabits these artificial reefs. This unique diving experience offers a glimpse into the island's past and the beauty of its underwater ecosystems.", + "locationName": "Bell Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "newfoundland", + "ref": "scuba-diving-at-bell-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_scuba-diving-at-bell-island.jpg" + }, + { + "name": "Explore the Quidi Vidi Village and Brewery", + "description": "Step back in time and visit the charming Quidi Vidi Village, a historic fishing community near St. John's. Stroll through the colorful houses, browse local crafts, and enjoy fresh seafood at the waterfront restaurants. Don't miss a visit to the Quidi Vidi Brewery, where you can sample award-winning craft beers and learn about the brewing process.", + "locationName": "Quidi Vidi Village", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "explore-the-quidi-vidi-village-and-brewery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_explore-the-quidi-vidi-village-and-brewery.jpg" + }, + { + "name": "Journey to the Viking Settlement of L'Anse aux Meadows", + "description": "Travel back in time and discover the only authenticated Viking settlement in North America. Explore the reconstructed Norse buildings, learn about the lives of these early explorers, and imagine their journey across the Atlantic. This historical and archaeological site offers a fascinating glimpse into Newfoundland's rich past.", + "locationName": "L'Anse aux Meadows", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "newfoundland", + "ref": "journey-to-the-viking-settlement-of-l-anse-aux-meadows", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_journey-to-the-viking-settlement-of-l-anse-aux-meadows.jpg" + }, + { + "name": "Road Trip Along the Irish Loop", + "description": "Embark on a scenic road trip along the Irish Loop, a coastal route known for its breathtaking landscapes and charming villages. Visit the Colony of Avalon, a 17th-century English settlement, explore the Cape St. Mary's Ecological Reserve with its thousands of seabirds, and enjoy fresh seafood in the picturesque town of Ferryland.", + "locationName": "Irish Loop", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "road-trip-along-the-irish-loop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_road-trip-along-the-irish-loop.jpg" + }, + { + "name": "Boat Tour of the Iceberg Alley", + "description": "Embark on a breathtaking boat tour along Iceberg Alley, where you can witness the majestic beauty of icebergs up close. Marvel at these colossal giants as they drift down from the Arctic, creating a surreal and unforgettable experience.", + "locationName": "Iceberg Alley (various departure points)", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "newfoundland", + "ref": "boat-tour-of-the-iceberg-alley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_boat-tour-of-the-iceberg-alley.jpg" + }, + { + "name": "Attend a Traditional Kitchen Party", + "description": "Immerse yourself in the lively spirit of Newfoundland culture by attending a traditional kitchen party. Enjoy toe-tapping music, lively dancing, and warm hospitality as locals share stories, songs, and laughter. This is a perfect opportunity to experience the genuine warmth and friendliness of the Newfoundland people.", + "locationName": "Various locations throughout Newfoundland", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "attend-a-traditional-kitchen-party", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_attend-a-traditional-kitchen-party.jpg" + }, + { + "name": "Explore the Bonavista Peninsula", + "description": "Discover the scenic beauty and rich history of the Bonavista Peninsula. Visit charming coastal towns like Trinity and Bonavista, where you can explore historic sites, enjoy local seafood, and witness stunning ocean views. Keep an eye out for puffins and other seabirds that inhabit the area.", + "locationName": "Bonavista Peninsula", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "explore-the-bonavista-peninsula", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_explore-the-bonavista-peninsula.jpg" + }, + { + "name": "Go Fishing in a Remote Outport", + "description": "Experience the traditional way of life in a remote Newfoundland outport by joining a local fishing excursion. Learn about the fishing industry, try your hand at catching cod or other species, and enjoy the tranquility of the surrounding ocean.", + "locationName": "Various outport communities", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "newfoundland", + "ref": "go-fishing-in-a-remote-outport", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_go-fishing-in-a-remote-outport.jpg" + }, + { + "name": "Visit Signal Hill National Historic Site", + "description": "Explore Signal Hill National Historic Site, a landmark overlooking St. John's harbor. Learn about the site's rich history, including its role in transatlantic communication and wartime defense. Enjoy panoramic views of the city and surrounding coastline.", + "locationName": "St. John's", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "newfoundland", + "ref": "visit-signal-hill-national-historic-site", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_visit-signal-hill-national-historic-site.jpg" + }, + { + "name": "Berry Picking Adventure", + "description": "Embark on a delightful journey through Newfoundland's wild landscapes, foraging for delicious berries like blueberries, partridgeberries, and bakeapples. Immerse yourself in the island's natural beauty while enjoying a unique and rewarding experience. Perfect for families and nature enthusiasts, berry picking offers a taste of Newfoundland's bounty and a chance to connect with the land.", + "locationName": "Various locations across the island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "newfoundland", + "ref": "berry-picking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_berry-picking-adventure.jpg" + }, + { + "name": "Geological Wonders Tour", + "description": "Explore the fascinating geological formations of Newfoundland, from the ancient rock formations of Gros Morne National Park to the dramatic cliffs of the Bonavista Peninsula. Discover the island's unique geological history, learn about plate tectonics, and marvel at the forces that have shaped this incredible landscape. This tour is perfect for those interested in science, nature, and the wonders of the Earth.", + "locationName": "Gros Morne National Park, Bonavista Peninsula", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "geological-wonders-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_geological-wonders-tour.jpg" + }, + { + "name": "Sea Kayaking Along the Coast", + "description": "Embark on a serene sea kayaking adventure along Newfoundland's stunning coastline. Paddle through crystal-clear waters, explore hidden coves, and admire the rugged cliffs and picturesque fishing villages. Keep an eye out for marine life such as whales, seals, and seabirds. This activity is perfect for those seeking a peaceful and immersive experience in nature.", + "locationName": "Various coastal locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "newfoundland", + "ref": "sea-kayaking-along-the-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_sea-kayaking-along-the-coast.jpg" + }, + { + "name": "Discover the Arts and Crafts Scene", + "description": "Immerse yourself in Newfoundland's vibrant arts and crafts scene. Visit local studios and galleries, meet talented artists, and discover unique handcrafted treasures. From pottery and paintings to textiles and woodwork, you'll find a diverse range of artistic expressions that reflect the island's rich culture and heritage. This activity is perfect for art lovers and those seeking unique souvenirs.", + "locationName": "St. John's, Twillingate, Trinity", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "newfoundland", + "ref": "discover-the-arts-and-crafts-scene", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_discover-the-arts-and-crafts-scene.jpg" + }, + { + "name": "Indulge in a Traditional Newfoundland Feast", + "description": "Treat your taste buds to a traditional Newfoundland feast, savoring local delicacies such as Jiggs dinner, fish and brewis, toutons, and cod tongues. Experience the unique flavors of the island's cuisine and learn about the culinary traditions that have been passed down through generations. This activity is perfect for foodies and those seeking an authentic cultural experience.", + "locationName": "Various restaurants across the island", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "newfoundland", + "ref": "indulge-in-a-traditional-newfoundland-feast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/newfoundland_indulge-in-a-traditional-newfoundland-feast.jpg" + }, + { + "name": "Surf the Pacific Coast", + "description": "Experience the thrill of riding the waves on Nicaragua's Pacific coast, renowned for its world-class surfing conditions. Whether you're a seasoned pro or a beginner, there are breaks suitable for all levels. Popular spots like San Juan del Sur and Popoyo offer consistent swells and a vibrant surf culture.", + "locationName": "San Juan del Sur or Popoyo", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "nicaragua", + "ref": "surf-the-pacific-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_surf-the-pacific-coast.jpg" + }, + { + "name": "Explore Colonial Granada", + "description": "Step back in time and wander through the charming streets of Granada, one of the oldest colonial cities in Central America. Admire the colorful architecture, visit historic churches and convents, and soak up the vibrant atmosphere of this cultural gem.", + "locationName": "Granada", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "nicaragua", + "ref": "explore-colonial-granada", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_explore-colonial-granada.jpg" + }, + { + "name": "Kayak on Lake Nicaragua", + "description": "Embark on a peaceful kayaking adventure on the vast Lake Nicaragua, the largest lake in Central America. Paddle through calm waters, surrounded by stunning scenery and diverse wildlife. Explore the numerous islands dotting the lake, including Ometepe, known for its twin volcanoes and unique ecosystem.", + "locationName": "Lake Nicaragua", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "nicaragua", + "ref": "kayak-on-lake-nicaragua", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_kayak-on-lake-nicaragua.jpg" + }, + { + "name": "Hike in the Cloud Forest of Reserva Natural Miraflor", + "description": "Immerse yourself in the lush beauty of the cloud forest at Reserva Natural Miraflor. Hike through trails teeming with diverse flora and fauna, including orchids, monkeys, and birds. Enjoy breathtaking views of the surrounding mountains and learn about the conservation efforts protecting this unique ecosystem.", + "locationName": "Reserva Natural Miraflor", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "nicaragua", + "ref": "hike-in-the-cloud-forest-of-reserva-natural-miraflor", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_hike-in-the-cloud-forest-of-reserva-natural-miraflor.jpg" + }, + { + "name": "Snorkel or Scuba Dive the Corn Islands", + "description": "Discover the vibrant underwater world of the Corn Islands, located off Nicaragua's Caribbean coast. With pristine coral reefs teeming with colorful fish and diverse marine life, these islands offer an unforgettable experience for both beginner and experienced snorkelers and scuba divers.", + "locationName": "Corn Islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "nicaragua", + "ref": "snorkel-or-scuba-dive-the-corn-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_snorkel-or-scuba-dive-the-corn-islands.jpg" + }, + { + "name": "Visit the Masaya Volcano National Park", + "description": "Embark on a thrilling journey to Masaya Volcano National Park, home to an active volcano with a mesmerizing lava lake. Witness the raw power of nature and explore the volcanic craters, lava tubes, and diverse ecosystems within the park. For an extra special experience, visit at night to see the glowing lava.", + "locationName": "Masaya Volcano National Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "nicaragua", + "ref": "visit-the-masaya-volcano-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_visit-the-masaya-volcano-national-park.jpg" + }, + { + "name": "Indulge in a Culinary Tour of Leon", + "description": "Embark on a delectable culinary adventure through the vibrant city of Leon. Sample traditional Nicaraguan dishes like vigoron, nacatamal, and quesillos, and discover the rich flavors and culinary heritage of the region. Explore local markets, street food stalls, and family-run restaurants to experience the authentic tastes of Nicaragua.", + "locationName": "Leon", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "nicaragua", + "ref": "indulge-in-a-culinary-tour-of-leon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_indulge-in-a-culinary-tour-of-leon.jpg" + }, + { + "name": "Relax at the Apoyo Lagoon", + "description": "Escape to the serene beauty of Apoyo Lagoon, a volcanic crater lake known for its crystal-clear waters and tranquil atmosphere. Enjoy swimming, kayaking, or simply lounging by the lakeshore surrounded by lush vegetation. Several eco-lodges and resorts offer stunning views and opportunities for relaxation and rejuvenation.", + "locationName": "Apoyo Lagoon", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "nicaragua", + "ref": "relax-at-the-apoyo-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_relax-at-the-apoyo-lagoon.jpg" + }, + { + "name": "Go Birdwatching in the Indio Maiz Biological Reserve", + "description": "Immerse yourself in the rich biodiversity of the Indio Maiz Biological Reserve, a pristine rainforest teeming with exotic birdlife. Embark on a guided birdwatching tour and spot toucans, macaws, parrots, and numerous other species in their natural habitat. The reserve also offers opportunities for wildlife viewing, hiking, and exploring the rainforest ecosystem.", + "locationName": "Indio Maiz Biological Reserve", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "nicaragua", + "ref": "go-birdwatching-in-the-indio-maiz-biological-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_go-birdwatching-in-the-indio-maiz-biological-reserve.jpg" + }, + { + "name": "Horseback Riding through the Countryside", + "description": "Embark on a scenic horseback riding adventure through the Nicaraguan countryside. Explore rolling hills, lush farmlands, and charming villages, all while enjoying the fresh air and stunning natural beauty. This activity is perfect for nature lovers and those seeking a peaceful escape.", + "locationName": "Various locations throughout Nicaragua", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "nicaragua", + "ref": "horseback-riding-through-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_horseback-riding-through-the-countryside.jpg" + }, + { + "name": "Sunset Catamaran Cruise", + "description": "Set sail on a romantic catamaran cruise and witness the breathtaking beauty of a Nicaraguan sunset over the Pacific Ocean. Enjoy the gentle sea breeze, sip on refreshing cocktails, and savor delicious appetizers as the sky transforms into a canvas of vibrant colors.", + "locationName": "Pacific Coast", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "nicaragua", + "ref": "sunset-catamaran-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_sunset-catamaran-cruise.jpg" + }, + { + "name": "Learn to Surf at a Surf Camp", + "description": "Catch some waves and learn to surf at a renowned surf camp along Nicaragua's Pacific coast. With experienced instructors and ideal conditions, you'll be riding the waves in no time. This activity is perfect for adventure seekers and those looking to try something new.", + "locationName": "San Juan del Sur or other surf spots", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "nicaragua", + "ref": "learn-to-surf-at-a-surf-camp", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_learn-to-surf-at-a-surf-camp.jpg" + }, + { + "name": "Visit the Flor de Caña Rum Distillery", + "description": "Take a tour of the Flor de Caña Rum Distillery and discover the secrets behind Nicaragua's most famous rum. Learn about the rum-making process, from sugarcane cultivation to distillation and aging, and enjoy a tasting of their premium rums.", + "locationName": "Chichigalpa", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "nicaragua", + "ref": "visit-the-flor-de-ca-a-rum-distillery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_visit-the-flor-de-ca-a-rum-distillery.jpg" + }, + { + "name": "Shop for Local Crafts at the Masaya Artisan Market", + "description": "Immerse yourself in the vibrant atmosphere of the Masaya Artisan Market and discover a treasure trove of locally made crafts. From handcrafted pottery and textiles to intricate wood carvings and jewelry, you'll find unique souvenirs and gifts to take home.", + "locationName": "Masaya", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "nicaragua", + "ref": "shop-for-local-crafts-at-the-masaya-artisan-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_shop-for-local-crafts-at-the-masaya-artisan-market.jpg" + }, + { + "name": "White-Water Rafting on the Rio San Juan", + "description": "Embark on an exhilarating white-water rafting adventure down the Rio San Juan, a stunning river that forms part of the border between Nicaragua and Costa Rica. Navigate through lush rainforests, spot diverse wildlife, and experience the thrill of rapids as you paddle downstream. This activity is perfect for adventure seekers and nature lovers.", + "locationName": "Rio San Juan", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "nicaragua", + "ref": "white-water-rafting-on-the-rio-san-juan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_white-water-rafting-on-the-rio-san-juan.jpg" + }, + { + "name": "Explore the Ruins of Leon Viejo", + "description": "Step back in time and discover the fascinating ruins of Leon Viejo, the original site of Nicaragua's first capital city. Founded in 1524 and later abandoned due to volcanic activity, the ruins offer a glimpse into the colonial past. Explore the excavated buildings, learn about the city's history, and enjoy panoramic views of the surrounding landscape.", + "locationName": "Leon Viejo", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "nicaragua", + "ref": "explore-the-ruins-of-leon-viejo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_explore-the-ruins-of-leon-viejo.jpg" + }, + { + "name": "Take a Chocolate Making Class", + "description": "Indulge your sweet tooth and discover the art of chocolate making with a hands-on workshop. Learn about the history of cacao in Nicaragua, participate in the bean-to-bar process, and create your own delicious chocolate treats to take home. This activity is perfect for families and foodies alike.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "nicaragua", + "ref": "take-a-chocolate-making-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_take-a-chocolate-making-class.jpg" + }, + { + "name": "Go Stargazing in the Dark Sky Reserves", + "description": "Escape the city lights and experience the magic of stargazing in one of Nicaragua's dark sky reserves. With minimal light pollution, these reserves offer breathtaking views of the Milky Way and constellations. Join a guided tour or venture out on your own for a truly awe-inspiring experience.", + "locationName": "Isla de Ometepe or other dark sky reserves", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "nicaragua", + "ref": "go-stargazing-in-the-dark-sky-reserves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_go-stargazing-in-the-dark-sky-reserves.jpg" + }, + { + "name": "Visit the Somoto Canyon National Monument", + "description": "Explore the hidden gem of Somoto Canyon National Monument, a stunning natural wonder with towering cliffs, crystal-clear waters, and diverse wildlife. Hike along the canyon rim, take a refreshing dip in the river, or go rock climbing for an adrenaline rush. This off-the-beaten-path destination offers a unique and unforgettable experience.", + "locationName": "Somoto Canyon National Monument", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "nicaragua", + "ref": "visit-the-somoto-canyon-national-monument", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/nicaragua_visit-the-somoto-canyon-national-monument.jpg" + }, + { + "name": "Yellow Water Cruise", + "description": "Embark on a serene sunrise or sunset cruise across the Yellow Water billabong, a haven for diverse wildlife. Witness saltwater crocodiles basking in the sun, colorful birds soaring overhead, and vibrant lotus flowers adorning the water's surface. Knowledgeable guides share insights into the ecosystem and Aboriginal culture, making it a captivating experience for all ages.", + "locationName": "Yellow Water", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "northern-territory", + "ref": "yellow-water-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_yellow-water-cruise.jpg" + }, + { + "name": "Nourlangie Rock Art Walk", + "description": "Step back in time and explore the ancient Aboriginal rock art galleries of Nourlangie Rock. Marvel at the intricate paintings that depict creation stories, animals, and traditional ways of life. Learn about the significance of these sites to the Aboriginal people and gain a deeper understanding of their connection to the land.", + "locationName": "Nourlangie Rock", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "northern-territory", + "ref": "nourlangie-rock-art-walk", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_nourlangie-rock-art-walk.jpg" + }, + { + "name": "Jim Jim Falls Hike and Swim", + "description": "Embark on a moderately challenging hike through monsoon forests to reach the magnificent Jim Jim Falls. Be rewarded with breathtaking views of the cascading waterfall and a refreshing dip in the plunge pool below. This adventure is perfect for those seeking an active experience amidst stunning natural beauty.", + "locationName": "Jim Jim Falls", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "northern-territory", + "ref": "jim-jim-falls-hike-and-swim", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_jim-jim-falls-hike-and-swim.jpg" + }, + { + "name": "Gunlom Plunge Pool Experience", + "description": "Take a scenic drive and short hike to discover the Gunlom Plunge Pool, a natural infinity pool with panoramic views of the surrounding landscape. Relax in the crystal-clear waters, soak up the sun, and enjoy a picnic lunch amidst the tranquil ambiance. The perfect escape for a relaxing afternoon.", + "locationName": "Gunlom Falls", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "northern-territory", + "ref": "gunlom-plunge-pool-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_gunlom-plunge-pool-experience.jpg" + }, + { + "name": "Warradjan Cultural Centre", + "description": "Immerse yourself in Aboriginal culture at the Warradjan Cultural Centre. Explore exhibits showcasing traditional art, artifacts, and stories of the Bininj/Mungguy people. Learn about their customs, beliefs, and connection to the land. The center also offers workshops and demonstrations, providing a deeper understanding of Aboriginal heritage.", + "locationName": "Bowali Visitor Centre", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "northern-territory", + "ref": "warradjan-cultural-centre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_warradjan-cultural-centre.jpg" + }, + { + "name": "Maguk Gorge Hike and Swim", + "description": "Embark on a scenic hike through monsoon rainforest to reach the secluded Maguk Gorge. Cool off with a refreshing dip in the crystal-clear plunge pool beneath the cascading waterfall. The trail offers stunning views of the surrounding escarpment and the opportunity to spot unique wildlife.", + "locationName": "Maguk", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "northern-territory", + "ref": "maguk-gorge-hike-and-swim", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_maguk-gorge-hike-and-swim.jpg" + }, + { + "name": "Cahill's Crossing Lookout", + "description": "Witness the thrilling spectacle of saltwater crocodiles at Cahill's Crossing. From the safety of the viewing platform, observe these powerful creatures as they gather to hunt for fish during the changing tides. This is a prime location for wildlife photography and an unforgettable experience.", + "locationName": "Cahill's Crossing", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "northern-territory", + "ref": "cahill-s-crossing-lookout", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_cahill-s-crossing-lookout.jpg" + }, + { + "name": "Ubirr Rock Art and Sunset Viewing", + "description": "Discover the ancient Aboriginal rock art galleries at Ubirr, showcasing thousands of years of cultural heritage. Ascend the rocky outcrop for panoramic views of the Nadab floodplain and witness a breathtaking sunset over the vast landscape. This combination of cultural immersion and natural beauty is truly unforgettable.", + "locationName": "Ubirr", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "northern-territory", + "ref": "ubirr-rock-art-and-sunset-viewing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_ubirr-rock-art-and-sunset-viewing.jpg" + }, + { + "name": "Mamukala Wetlands Birdwatching", + "description": "Explore the Mamukala Wetlands, a haven for birdwatchers. Home to a diverse range of waterbirds, including magpie geese, jacanas, and comb-crested jacanas, this is a paradise for nature enthusiasts. Enjoy the serenity of the wetlands while observing these feathered creatures in their natural habitat.", + "locationName": "Mamukala Wetlands", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "northern-territory", + "ref": "mamukala-wetlands-birdwatching", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_mamukala-wetlands-birdwatching.jpg" + }, + { + "name": "Nighttime Wildlife Spotting Tour", + "description": "Embark on a guided nocturnal adventure to discover the fascinating creatures that come alive after dark. Spot nocturnal wildlife like owls, possums, and bandicoots with the help of experienced guides. This unique tour offers a glimpse into the hidden world of Kakadu's nighttime ecosystem.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "northern-territory", + "ref": "nighttime-wildlife-spotting-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_nighttime-wildlife-spotting-tour.jpg" + }, + { + "name": "Barramundi Fishing Adventure", + "description": "Embark on an exhilarating fishing expedition in the heart of Kakadu. Join experienced local guides who will lead you to prime fishing spots, where you'll have the opportunity to cast your line for the iconic barramundi, a prized catch known for its fighting spirit. Whether you're a seasoned angler or a beginner, this adventure promises excitement and a chance to connect with the region's abundant aquatic life.", + "locationName": "Various waterways within Kakadu National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "northern-territory", + "ref": "barramundi-fishing-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_barramundi-fishing-adventure.jpg" + }, + { + "name": "4WD Off-Road Exploration", + "description": "Venture off the beaten path and discover the rugged beauty of Kakadu's outback on an exhilarating 4WD tour. Traverse through diverse landscapes, from floodplains and woodlands to rocky escarpments, as you encounter hidden waterfalls, ancient rock art sites, and breathtaking panoramic views. This adventure offers a unique perspective of the park's vastness and remoteness.", + "locationName": "Various off-road tracks within Kakadu National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "northern-territory", + "ref": "4wd-off-road-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_4wd-off-road-exploration.jpg" + }, + { + "name": "Bush Tucker Walk and Aboriginal Culture", + "description": "Immerse yourself in the rich cultural heritage of Kakadu's Aboriginal people with a guided bush tucker walk. Learn about traditional uses of native plants for food, medicine, and tools, and gain insights into the deep connection between the land and its people. This experience provides a unique opportunity to appreciate the cultural significance of Kakadu and its ancient traditions.", + "locationName": "Various locations within Kakadu National Park", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "northern-territory", + "ref": "bush-tucker-walk-and-aboriginal-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_bush-tucker-walk-and-aboriginal-culture.jpg" + }, + { + "name": "Scenic Helicopter Flight", + "description": "Soar above the breathtaking landscapes of Kakadu on a scenic helicopter flight. Witness the grandeur of cascading waterfalls, sandstone escarpments, and vast floodplains from a unique aerial perspective. Capture stunning panoramic views and create unforgettable memories as you explore the park's diverse ecosystems and iconic landmarks.", + "locationName": "Various locations within Kakadu National Park", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "northern-territory", + "ref": "scenic-helicopter-flight", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_scenic-helicopter-flight.jpg" + }, + { + "name": "Stargazing and Astronomy Tour", + "description": "Escape the city lights and embark on a magical stargazing adventure in the pristine night sky of Kakadu. Join an expert astronomer who will guide you through the constellations, planets, and celestial wonders visible in the Southern Hemisphere. Learn about Aboriginal astronomy and the significance of the stars in their culture, while marveling at the beauty of the cosmos.", + "locationName": "Various locations within Kakadu National Park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "northern-territory", + "ref": "stargazing-and-astronomy-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_stargazing-and-astronomy-tour.jpg" + }, + { + "name": "Indigenous Art Workshops", + "description": "Immerse yourself in the rich artistic traditions of Kakadu's Aboriginal people by participating in an Indigenous art workshop. Learn about the symbolism behind traditional dot paintings and rock art, and create your own masterpiece using natural pigments and techniques passed down through generations.", + "locationName": "Various art centres within the park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "northern-territory", + "ref": "indigenous-art-workshops", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_indigenous-art-workshops.jpg" + }, + { + "name": "Bush Tucker Foraging and Cooking Experience", + "description": "Embark on a guided journey through the bush to discover the edible plants and fruits that have sustained Aboriginal people for millennia. Learn about traditional foraging methods and the medicinal properties of native flora, and then participate in a cooking demonstration to savor the unique flavors of bush tucker.", + "locationName": "Various locations within the park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "northern-territory", + "ref": "bush-tucker-foraging-and-cooking-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_bush-tucker-foraging-and-cooking-experience.jpg" + }, + { + "name": "Kakadu Photography Tour", + "description": "Capture the stunning landscapes and unique wildlife of Kakadu on a photography tour led by a professional photographer. Learn about composition, lighting, and wildlife photography techniques, and receive expert guidance on capturing the essence of this remarkable national park.", + "locationName": "Various locations within the park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "northern-territory", + "ref": "kakadu-photography-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_kakadu-photography-tour.jpg" + }, + { + "name": "Yellow Water Billabong Sunset Cruise", + "description": "Experience the magic of a Kakadu sunset on a tranquil cruise through the Yellow Water Billabong. As the sun dips below the horizon, witness the abundant wildlife come alive, including crocodiles, birds, and wallabies. Enjoy the serenity of the wetlands and the vibrant colors of the evening sky.", + "locationName": "Yellow Water Billabong", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "northern-territory", + "ref": "yellow-water-billabong-sunset-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_yellow-water-billabong-sunset-cruise.jpg" + }, + { + "name": "Arnhem Land Day Trip", + "description": "Venture beyond Kakadu National Park on a day trip to Arnhem Land, a vast Aboriginal reserve with restricted access. Immerse yourself in the rich culture and traditions of the Yolngu people, learn about their ancient rock art, and witness traditional ceremonies and dances.", + "locationName": "Arnhem Land", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "northern-territory", + "ref": "arnhem-land-day-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/northern-territory_arnhem-land-day-trip.jpg" + }, + { + "name": "Explore Monte Albán", + "description": "Step back in time at Monte Albán, a UNESCO World Heritage Site and ancient Zapotec capital. Marvel at the impressive pyramids, temples, and plazas that offer panoramic views of the Oaxaca Valley. Learn about the fascinating history and culture of this pre-Columbian civilization.", + "locationName": "Monte Albán Archaeological Site", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "oaxaca", + "ref": "explore-monte-alb-n", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_explore-monte-alb-n.jpg" + }, + { + "name": "Wander Through the Mercado 20 de Noviembre and Benito Juárez Markets", + "description": "Immerse yourself in the vibrant atmosphere of Oaxaca's bustling markets. Browse through a colorful array of handicrafts, textiles, ceramics, and local produce. Savor the aromas of Oaxacan cuisine and sample traditional delicacies like chapulines (grasshoppers) and tejate (a pre-Hispanic drink).", + "locationName": "Mercado 20 de Noviembre and Benito Juárez Markets", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "oaxaca", + "ref": "wander-through-the-mercado-20-de-noviembre-and-benito-ju-rez-markets", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_wander-through-the-mercado-20-de-noviembre-and-benito-ju-rez-markets.jpg" + }, + { + "name": "Discover the Tule Tree", + "description": "Visit the Árbol del Tule, a majestic cypress tree with one of the stoutest trunks in the world. Admire its immense size and unique shape, and learn about its cultural significance to the local community.", + "locationName": "Santa María del Tule", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "oaxaca", + "ref": "discover-the-tule-tree", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_discover-the-tule-tree.jpg" + }, + { + "name": "Delve into Oaxacan Cuisine", + "description": "Embark on a culinary journey and discover the rich flavors of Oaxacan cuisine. Take a cooking class to learn the secrets of mole negro, tlayudas, and other regional specialties. Visit traditional restaurants and mezcalerias to savor authentic dishes and sample the smoky notes of mezcal.", + "locationName": "Various locations throughout Oaxaca", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "oaxaca", + "ref": "delve-into-oaxacan-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_delve-into-oaxacan-cuisine.jpg" + }, + { + "name": "Experience the Guelaguetza Festival", + "description": "If you're visiting Oaxaca in July, don't miss the Guelaguetza festival, a vibrant celebration of indigenous culture. Witness traditional dances, music, and costumes from different regions of Oaxaca. Immerse yourself in the festive atmosphere and learn about the state's diverse heritage.", + "locationName": "Auditorio Guelaguetza", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "oaxaca", + "ref": "experience-the-guelaguetza-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_experience-the-guelaguetza-festival.jpg" + }, + { + "name": "Hike Hierve el Agua", + "description": "Embark on a scenic hike to Hierve el Agua, a mesmerizing natural wonder with petrified waterfalls and mineral-rich pools. Enjoy breathtaking views of the surrounding valleys and take a refreshing dip in the pools known for their therapeutic properties.", + "locationName": "Hierve el Agua", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "oaxaca", + "ref": "hike-hierve-el-agua", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_hike-hierve-el-agua.jpg" + }, + { + "name": "Visit Teotitlán del Valle", + "description": "Immerse yourself in the rich textile traditions of Oaxaca with a visit to Teotitlán del Valle, a village renowned for its hand-woven rugs and tapestries. Witness skilled artisans using natural dyes and traditional techniques to create intricate designs, and perhaps even purchase a unique piece to take home.", + "locationName": "Teotitlán del Valle", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "oaxaca", + "ref": "visit-teotitl-n-del-valle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_visit-teotitl-n-del-valle.jpg" + }, + { + "name": "Explore Mitla Archaeological Site", + "description": "Step back in time at the Mitla archaeological site, known for its intricate mosaics and geometric designs. Explore the ancient Zapotec city, admire the impressive architecture, and learn about the fascinating history and culture of the region.", + "locationName": "Mitla", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "oaxaca", + "ref": "explore-mitla-archaeological-site", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_explore-mitla-archaeological-site.jpg" + }, + { + "name": "Go Mezcal Tasting", + "description": "Indulge in the smoky flavors of mezcal, a traditional Oaxacan spirit made from agave. Visit a local palenque (distillery) to learn about the production process, from harvesting the agave to the distillation methods, and enjoy a guided tasting of different mezcal varieties.", + "locationName": "Various Palenques", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "oaxaca", + "ref": "go-mezcal-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_go-mezcal-tasting.jpg" + }, + { + "name": "Catch a Performance at Teatro Macedonio Alcalá", + "description": "Experience the cultural vibrancy of Oaxaca with a performance at the Teatro Macedonio Alcalá, a stunning neoclassical theater. Enjoy a variety of shows, including traditional dance, music concerts, and theatrical productions, showcasing the artistic talents of the region.", + "locationName": "Teatro Macedonio Alcalá", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "oaxaca", + "ref": "catch-a-performance-at-teatro-macedonio-alcal-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_catch-a-performance-at-teatro-macedonio-alcal-.jpg" + }, + { + "name": "Mountain Biking in the Sierra Norte", + "description": "Embark on an exhilarating mountain biking adventure through the stunning landscapes of the Sierra Norte mountains. Explore rugged trails, dense forests, and charming villages while enjoying breathtaking views. This activity is perfect for thrill-seekers and nature enthusiasts looking for an active and immersive experience.", + "locationName": "Sierra Norte Mountains", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "oaxaca", + "ref": "mountain-biking-in-the-sierra-norte", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_mountain-biking-in-the-sierra-norte.jpg" + }, + { + "name": "Relax at Hierve el Agua", + "description": "Unwind and rejuvenate at Hierve el Agua, a natural wonder with petrified waterfalls and mineral-rich pools. Take a dip in the warm waters, enjoy the panoramic views, and experience the unique geological formations. This is a perfect spot to relax, soak up the sun, and connect with nature.", + "locationName": "Hierve el Agua", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "oaxaca", + "ref": "relax-at-hierve-el-agua", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_relax-at-hierve-el-agua.jpg" + }, + { + "name": "Visit the Santo Domingo Cultural Center", + "description": "Immerse yourself in Oaxacan art and history at the Santo Domingo Cultural Center. Explore the stunning 16th-century Santo Domingo de Guzmán church, admire the intricate Baroque architecture, and visit the Oaxaca Cultures Museum to discover the region's rich cultural heritage.", + "locationName": "Santo Domingo Cultural Center", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "oaxaca", + "ref": "visit-the-santo-domingo-cultural-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_visit-the-santo-domingo-cultural-center.jpg" + }, + { + "name": "Take a Cooking Class", + "description": "Learn the secrets of Oaxacan cuisine by taking a cooking class. Discover the traditional ingredients and techniques used to prepare iconic dishes like mole, tlayudas, and tamales. This hands-on experience is perfect for food enthusiasts who want to delve deeper into the culinary traditions of Oaxaca.", + "locationName": "Various locations in Oaxaca City", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "oaxaca", + "ref": "take-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_take-a-cooking-class.jpg" + }, + { + "name": "Explore the Ruins of Yagul", + "description": "Step back in time at the archaeological site of Yagul, a UNESCO World Heritage Site. Explore the ancient Zapotec city, including palaces, temples, and ball courts. Discover the fascinating history and culture of this pre-Columbian civilization while enjoying panoramic views of the surrounding valley.", + "locationName": "Yagul Archaeological Site", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "oaxaca", + "ref": "explore-the-ruins-of-yagul", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_explore-the-ruins-of-yagul.jpg" + }, + { + "name": "Horseback Riding in the Countryside", + "description": "Embark on a scenic horseback riding adventure through the picturesque Oaxacan countryside. Traverse rolling hills, agave fields, and charming villages, immersing yourself in the natural beauty and rural life of the region. This activity is suitable for all skill levels, offering a unique perspective on the landscape and a chance to connect with nature.", + "locationName": "Oaxacan Countryside", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "oaxaca", + "ref": "horseback-riding-in-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_horseback-riding-in-the-countryside.jpg" + }, + { + "name": "Birdwatching in the Sierra Norte", + "description": "Discover the rich avian diversity of the Sierra Norte mountains on a guided birdwatching tour. Explore cloud forests and diverse ecosystems, spotting endemic species and migratory birds. Learn about the region's conservation efforts and the importance of preserving these natural habitats. This activity is perfect for nature enthusiasts and photographers seeking to capture the beauty of Oaxaca's feathered residents.", + "locationName": "Sierra Norte Mountains", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "oaxaca", + "ref": "birdwatching-in-the-sierra-norte", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_birdwatching-in-the-sierra-norte.jpg" + }, + { + "name": "Sunset at the Cerro del Fortín", + "description": "Ascend the Cerro del Fortín, a hilltop overlooking the city of Oaxaca, and witness a breathtaking sunset. Enjoy panoramic views of the city, surrounding valleys, and distant mountains as the sky transforms with vibrant colors. This romantic and picturesque spot is ideal for capturing memorable photos and enjoying a peaceful moment.", + "locationName": "Cerro del Fortín", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "oaxaca", + "ref": "sunset-at-the-cerro-del-fort-n", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_sunset-at-the-cerro-del-fort-n.jpg" + }, + { + "name": "Stargazing in the Desert", + "description": "Escape the city lights and embark on a stargazing adventure in the Oaxacan desert. With minimal light pollution, the night sky comes alive with countless stars, constellations, and celestial wonders. Join a guided tour or find a secluded spot to marvel at the vastness of the universe and learn about the constellations visible from this region.", + "locationName": "Oaxacan Desert", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "oaxaca", + "ref": "stargazing-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_stargazing-in-the-desert.jpg" + }, + { + "name": "Temazcal Ceremony", + "description": "Participate in a traditional Temazcal ceremony, a pre-Hispanic steam bath ritual used for physical and spiritual purification. Enter a dome-shaped structure and experience the healing properties of herbs and steam. This ancient practice is led by a shaman and offers a unique opportunity to connect with indigenous traditions and promote well-being.", + "locationName": "Various Locations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "oaxaca", + "ref": "temazcal-ceremony", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oaxaca_temazcal-ceremony.jpg" + }, + { + "name": "Mokoro Safari", + "description": "Embark on a serene mokoro excursion through the tranquil waterways of the Okavango Delta. Glide past papyrus reeds and water lilies as you observe elephants bathing, hippos wallowing, and crocodiles basking in the sun. Your experienced poler will navigate the channels, sharing their knowledge of the delta's ecosystem and its inhabitants.", + "locationName": "Okavango Delta", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "okavango-delta", + "ref": "mokoro-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_mokoro-safari.jpg" + }, + { + "name": "Bush Walks and Wildlife Tracking", + "description": "Venture into the heart of the delta on a guided bush walk, accompanied by skilled trackers. Learn to identify animal tracks, interpret their behavior, and discover the intricate relationships within the ecosystem. Spot giraffes gracefully browsing on acacia leaves, zebras grazing in the open plains, and perhaps even lions resting in the shade.", + "locationName": "Moremi Game Reserve", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "okavango-delta", + "ref": "bush-walks-and-wildlife-tracking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_bush-walks-and-wildlife-tracking.jpg" + }, + { + "name": "Scenic Helicopter Flight", + "description": "Soar above the sprawling Okavango Delta in a helicopter, gaining a breathtaking aerial perspective of its vastness and intricate network of waterways. Witness the mosaic of islands, floodplains, and forests, and marvel at the herds of elephants, buffalo, and antelope roaming freely below.", + "locationName": "Various locations", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "okavango-delta", + "ref": "scenic-helicopter-flight", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_scenic-helicopter-flight.jpg" + }, + { + "name": "Birdwatching Expedition", + "description": "Join a dedicated birdwatching tour to discover the Okavango Delta's incredible avian diversity. With over 400 species recorded, you'll encounter a kaleidoscope of colors and calls. Spot fish eagles soaring overhead, malachite kingfishers perched on reeds, and African jacanas delicately walking on lily pads.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "okavango-delta", + "ref": "birdwatching-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_birdwatching-expedition.jpg" + }, + { + "name": "Fishing in the Delta", + "description": "Cast your line into the pristine waters of the Okavango Delta and try your hand at catching tigerfish, bream, and catfish. Whether you're an experienced angler or a novice, fishing in this unique environment is an unforgettable experience.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "okavango-delta", + "ref": "fishing-in-the-delta", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_fishing-in-the-delta.jpg" + }, + { + "name": "Horseback Safari", + "description": "Embark on a unique safari experience on horseback, allowing you to get closer to nature and observe wildlife from a different perspective. Traverse the diverse landscapes of the delta, from floodplains to islands, and encounter animals like zebras, giraffes, and antelopes in their natural habitat. Experienced guides will ensure your safety and provide insights into the ecosystem.", + "locationName": "Okavango Delta", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "okavango-delta", + "ref": "horseback-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_horseback-safari.jpg" + }, + { + "name": "Stargazing Cruise", + "description": "Escape the city lights and embark on a magical stargazing cruise along the tranquil waterways of the delta. With minimal light pollution, the night sky comes alive with a breathtaking display of stars and constellations. Expert guides will share their knowledge of astronomy, while you enjoy the serenity of the surroundings.", + "locationName": "Okavango Delta", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "okavango-delta", + "ref": "stargazing-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_stargazing-cruise.jpg" + }, + { + "name": "Cultural Village Visit", + "description": "Immerse yourself in the rich cultural heritage of the local communities by visiting a traditional village. Interact with villagers, learn about their customs and way of life, and witness demonstrations of traditional crafts, music, and dance. This experience offers a unique opportunity to gain a deeper understanding of the delta's cultural significance.", + "locationName": "Local village", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "okavango-delta", + "ref": "cultural-village-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_cultural-village-visit.jpg" + }, + { + "name": "Hot Air Balloon Ride", + "description": "Soar above the breathtaking landscapes of the Okavango Delta in a hot air balloon. Witness the vastness of the delta from a unique aerial perspective, as you drift over floodplains, islands, and channels teeming with wildlife. Enjoy the tranquility of the early morning or the golden hues of sunset, creating unforgettable memories.", + "locationName": "Okavango Delta", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "okavango-delta", + "ref": "hot-air-balloon-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_hot-air-balloon-ride.jpg" + }, + { + "name": "Photography Safari", + "description": "Capture the beauty of the Okavango Delta through the lens of your camera on a dedicated photography safari. Accompanied by experienced guides and professional photographers, you'll have the opportunity to learn advanced photography techniques and capture stunning images of wildlife, landscapes, and local cultures.", + "locationName": "Okavango Delta", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "okavango-delta", + "ref": "photography-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_photography-safari.jpg" + }, + { + "name": "Quad Biking Adventure", + "description": "Embark on an exhilarating quad biking adventure through the diverse terrain of the Okavango Delta. Traverse sandy tracks, splash through shallow waters, and navigate lush vegetation as you explore the wilderness on four wheels. This thrilling experience offers a unique perspective of the delta's landscapes and the chance to encounter wildlife in their natural habitat.", + "locationName": "Various locations within the delta", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "okavango-delta", + "ref": "quad-biking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_quad-biking-adventure.jpg" + }, + { + "name": "Traditional Village Immersion", + "description": "Immerse yourself in the rich culture of the Okavango Delta by visiting a local village. Interact with the friendly residents, learn about their customs and traditions, and gain insights into their daily lives. Participate in activities such as basket weaving, traditional dancing, or storytelling, and discover the authentic essence of the delta's communities.", + "locationName": "Local villages within or near the delta", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "okavango-delta", + "ref": "traditional-village-immersion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_traditional-village-immersion.jpg" + }, + { + "name": "Scenic Boat Cruise", + "description": "Embark on a serene boat cruise along the tranquil waterways of the Okavango Delta. Relax and soak in the breathtaking scenery as you glide past lush vegetation, papyrus-lined channels, and islands teeming with wildlife. Keep an eye out for elephants, hippos, crocodiles, and a variety of bird species, all while enjoying the peaceful ambiance of the delta.", + "locationName": "Various locations within the delta", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "okavango-delta", + "ref": "scenic-boat-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_scenic-boat-cruise.jpg" + }, + { + "name": "Bush Camping Experience", + "description": "Spend a night under the stars with a thrilling bush camping experience in the heart of the Okavango Delta. Set up camp in a secluded location, surrounded by the sights and sounds of the African wilderness. Enjoy a campfire dinner, listen to the nocturnal sounds of the bush, and wake up to the stunning sunrise over the delta.", + "locationName": "Designated campsites within the delta", + "duration": 24, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "okavango-delta", + "ref": "bush-camping-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_bush-camping-experience.jpg" + }, + { + "name": "Wildlife Photography Workshop", + "description": "Join a wildlife photography workshop led by experienced professionals and capture stunning images of the Okavango Delta's diverse fauna. Learn valuable techniques for photographing animals in their natural habitat, including composition, lighting, and patience. This workshop is perfect for photography enthusiasts of all levels who want to improve their skills and create lasting memories of their safari adventure.", + "locationName": "Various locations within the delta", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "okavango-delta", + "ref": "wildlife-photography-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_wildlife-photography-workshop.jpg" + }, + { + "name": "Sleep-Out Under the Stars", + "description": "Embark on an unforgettable overnight adventure sleeping under the vast African sky. Enjoy a delicious bush dinner prepared over an open fire, share stories with your guide, and fall asleep to the symphony of nocturnal wildlife. Wake up to a breathtaking sunrise and the sounds of the delta coming to life.", + "locationName": "Remote island in the delta", + "duration": 24, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "okavango-delta", + "ref": "sleep-out-under-the-stars", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_sleep-out-under-the-stars.jpg" + }, + { + "name": "Community-Based Tourism Experience", + "description": "Immerse yourself in the local culture and traditions by visiting a nearby village. Interact with community members, learn about their way of life, participate in traditional crafts or activities, and support sustainable tourism initiatives.", + "locationName": "Local village near the delta", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "okavango-delta", + "ref": "community-based-tourism-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_community-based-tourism-experience.jpg" + }, + { + "name": "Scenic Helicopter Flight over the Delta", + "description": "Get a bird's-eye view of the Okavango Delta's intricate waterways, lush islands, and abundant wildlife. Soar above the landscape in a helicopter, capturing stunning aerial photographs and gaining a unique perspective of this natural wonder.", + "locationName": "Various locations in the delta", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "okavango-delta", + "ref": "scenic-helicopter-flight-over-the-delta", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_scenic-helicopter-flight-over-the-delta.jpg" + }, + { + "name": "Learn Traditional Fishing Techniques", + "description": "Join local fishermen and learn the art of traditional fishing methods used in the Okavango Delta. Discover the secrets of sustainable fishing practices, try your hand at catching local fish species, and gain insights into the importance of the delta's ecosystem.", + "locationName": "Fishing villages or camps along the delta", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "okavango-delta", + "ref": "learn-traditional-fishing-techniques", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_learn-traditional-fishing-techniques.jpg" + }, + { + "name": "Wildlife Photography Workshop", + "description": "Enhance your wildlife photography skills with a dedicated workshop led by experienced photographers. Learn about camera settings, composition techniques, and animal behavior to capture stunning images of the delta's diverse wildlife.", + "locationName": "Various lodges or camps in the delta", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "okavango-delta", + "ref": "wildlife-photography-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/okavango-delta_wildlife-photography-workshop.jpg" + }, + { + "name": "Wahiba Sands Adventure", + "description": "Embark on a thrilling 4x4 journey into the vast Wahiba Sands desert, where towering dunes shift with the desert winds. Experience the thrill of dune bashing, sandboarding down steep slopes, and witness the breathtaking sunset over the endless sandscape. Spend a night under the starlit sky in a traditional Bedouin camp, enjoying authentic Omani hospitality, music, and dance.", + "locationName": "Wahiba Sands", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "oman", + "ref": "wahiba-sands-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_wahiba-sands-adventure.jpg" + }, + { + "name": "Explore the Historic Forts of Nizwa and Bahla", + "description": "Step back in time and delve into Oman's rich history by visiting the impressive forts of Nizwa and Bahla. Explore the ancient architecture, intricate carvings, and hidden passages of these UNESCO World Heritage sites. Learn about the role these forts played in defending the region and gain insights into Omani culture and heritage.", + "locationName": "Nizwa and Bahla", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "oman", + "ref": "explore-the-historic-forts-of-nizwa-and-bahla", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_explore-the-historic-forts-of-nizwa-and-bahla.jpg" + }, + { + "name": "Dive into the Turquoise Waters of Wadi Shab", + "description": "Escape the desert heat and embark on a refreshing adventure to Wadi Shab, a stunning gorge with emerald-green pools and cascading waterfalls. Hike through the wadi, swim in the crystal-clear waters, and discover hidden caves. For the adventurous, climb up to the secret waterfall and take a plunge into the refreshing pool below.", + "locationName": "Wadi Shab", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "oman", + "ref": "dive-into-the-turquoise-waters-of-wadi-shab", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_dive-into-the-turquoise-waters-of-wadi-shab.jpg" + }, + { + "name": "Immerse Yourself in the Bustling Muttrah Souq", + "description": "Wander through the labyrinthine alleys of the Muttrah Souq, a traditional Omani market overflowing with vibrant sights, sounds, and aromas. Discover a treasure trove of handcrafted souvenirs, aromatic spices, and exquisite silver jewelry. Bargaining is expected, so hone your negotiation skills and find unique keepsakes to remember your Omani adventure.", + "locationName": "Muttrah Souq", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "oman", + "ref": "immerse-yourself-in-the-bustling-muttrah-souq", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_immerse-yourself-in-the-bustling-muttrah-souq.jpg" + }, + { + "name": "Stargazing in the Desert", + "description": "Escape the city lights and experience the magic of the desert night sky. Join a stargazing tour and marvel at the Milky Way stretching across the darkness. Learn about constellations, planets, and the wonders of the universe from knowledgeable guides. This unforgettable experience will leave you with a sense of awe and wonder.", + "locationName": "Wahiba Sands or other desert locations", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "oman", + "ref": "stargazing-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_stargazing-in-the-desert.jpg" + }, + { + "name": "Dolphin Watching in Musandam", + "description": "Embark on a thrilling boat trip through the fjords of Musandam, often called the 'Norway of Arabia'. Witness playful dolphins leaping alongside your vessel, surrounded by stunning landscapes of towering cliffs and crystal-clear waters. This unforgettable experience offers a unique perspective of Oman's diverse marine life and breathtaking natural beauty.", + "locationName": "Musandam Fjords", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "oman", + "ref": "dolphin-watching-in-musandam", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_dolphin-watching-in-musandam.jpg" + }, + { + "name": "Trekking in the Hajar Mountains", + "description": "Lace up your hiking boots and embark on an adventurous trek through the rugged Hajar Mountains. Discover hidden wadis, ancient villages, and breathtaking panoramic views. Challenge yourself with a climb to the summit of Jebel Shams, the highest peak in Oman, or opt for a gentler trail through scenic valleys. This experience is perfect for nature enthusiasts and adventure seekers.", + "locationName": "Hajar Mountains", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "oman", + "ref": "trekking-in-the-hajar-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_trekking-in-the-hajar-mountains.jpg" + }, + { + "name": "Explore the Frankincense Trail", + "description": "Journey through the ancient Frankincense Trail, a UNESCO World Heritage Site, and delve into Oman's rich history and cultural heritage. Visit the archaeological sites of Al Baleed and Sumhuram, wander through the Frankincense Land Museum, and explore the lush Wadi Dawkah, where frankincense trees still thrive. This experience offers a fascinating glimpse into Oman's legendary past and its connection to the ancient trade routes.", + "locationName": "Dhofar Region", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "oman", + "ref": "explore-the-frankincense-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_explore-the-frankincense-trail.jpg" + }, + { + "name": "Sunset Cruise in a Traditional Dhow", + "description": "Sail along the Omani coastline aboard a traditional dhow, a wooden sailing vessel, and witness a magical sunset over the Arabian Sea. Enjoy the gentle breeze, admire the picturesque scenery, and savor a delicious Omani dinner on board. This romantic and relaxing experience is perfect for couples or those seeking a tranquil escape.", + "locationName": "Muscat or Salalah", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "oman", + "ref": "sunset-cruise-in-a-traditional-dhow", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_sunset-cruise-in-a-traditional-dhow.jpg" + }, + { + "name": "Experience Omani Hospitality at a Local Farm", + "description": "Immerse yourself in Omani culture with a visit to a local farm. Learn about traditional farming practices, sample fresh produce, and enjoy a home-cooked meal with a local family. This authentic and heartwarming experience offers a glimpse into the daily lives of Omani people and their warm hospitality.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "oman", + "ref": "experience-omani-hospitality-at-a-local-farm", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_experience-omani-hospitality-at-a-local-farm.jpg" + }, + { + "name": "Discover the Underwater World of the Daymaniyat Islands", + "description": "Embark on a snorkeling or diving adventure to the Daymaniyat Islands, a protected nature reserve known for its pristine coral reefs and diverse marine life. Swim alongside colorful fish, encounter graceful sea turtles, and marvel at the vibrant underwater ecosystem.", + "locationName": "Daymaniyat Islands", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "oman", + "ref": "discover-the-underwater-world-of-the-daymaniyat-islands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_discover-the-underwater-world-of-the-daymaniyat-islands.jpg" + }, + { + "name": "Unwind at a Luxurious Spa Retreat", + "description": "Indulge in a rejuvenating spa experience at one of Oman's many luxurious resorts. Choose from a variety of treatments, including traditional hammam rituals, aromatherapy massages, and revitalizing facials. Let the expert therapists melt away your stress and leave you feeling refreshed and renewed.", + "locationName": "Various spa resorts", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "oman", + "ref": "unwind-at-a-luxurious-spa-retreat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_unwind-at-a-luxurious-spa-retreat.jpg" + }, + { + "name": "Visit the Sultan Qaboos Grand Mosque", + "description": "Witness the architectural grandeur of the Sultan Qaboos Grand Mosque, a masterpiece of modern Islamic design. Admire the intricate details, including the hand-woven prayer carpet and the stunning Swarovski crystal chandelier. Explore the peaceful courtyards and learn about Islamic culture and heritage.", + "locationName": "Muscat", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "oman", + "ref": "visit-the-sultan-qaboos-grand-mosque", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_visit-the-sultan-qaboos-grand-mosque.jpg" + }, + { + "name": "Go Sandboarding in the Sharqiya Sands", + "description": "Experience the thrill of sandboarding down the towering dunes of the Sharqiya Sands. Rent a board and try your hand at this exhilarating activity. Whether you're a beginner or an experienced boarder, the endless expanse of sand provides an unforgettable adventure.", + "locationName": "Sharqiya Sands", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "oman", + "ref": "go-sandboarding-in-the-sharqiya-sands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_go-sandboarding-in-the-sharqiya-sands.jpg" + }, + { + "name": "Explore the Jabal Akhdar Mountains", + "description": "Embark on a scenic drive or hike through the Jabal Akhdar Mountains, known as the \"Green Mountain\" for its terraced orchards and rose plantations. Visit traditional villages, enjoy breathtaking views, and discover the unique culture of the mountain communities.", + "locationName": "Jabal Akhdar Mountains", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "oman", + "ref": "explore-the-jabal-akhdar-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_explore-the-jabal-akhdar-mountains.jpg" + }, + { + "name": "Kayaking in the Fjords of Musandam", + "description": "Embark on a kayaking adventure through the stunning fjords of Musandam, often referred to as the 'Norway of Arabia'. Paddle through crystal-clear waters, surrounded by towering limestone cliffs, secluded beaches, and hidden coves. Encounter playful dolphins, colorful fish, and diverse marine life as you explore this breathtaking natural wonder.", + "locationName": "Musandam Peninsula", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "oman", + "ref": "kayaking-in-the-fjords-of-musandam", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_kayaking-in-the-fjords-of-musandam.jpg" + }, + { + "name": "Camel Racing in the Desert", + "description": "Experience the thrill of camel racing, a traditional Omani sport. Witness the incredible speed and agility of these majestic animals as they compete across the desert sands. Immerse yourself in the vibrant atmosphere, with cheering crowds and local festivities. You can even try your hand at riding a camel and get a taste of Bedouin life.", + "locationName": "Sharqiya Sands or other desert locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "oman", + "ref": "camel-racing-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_camel-racing-in-the-desert.jpg" + }, + { + "name": "Sample Omani Cuisine at a Local Restaurant", + "description": "Embark on a culinary journey through Oman by indulging in the local cuisine. Visit a traditional restaurant and savor authentic dishes like Shuwa (slow-cooked lamb), Machboos (spiced rice with meat), and Omani Halwa (sweet gelatinous dessert). Experience the unique flavors and spices that make Omani food a delightful experience.", + "locationName": "Muscat or other cities and towns", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "oman", + "ref": "sample-omani-cuisine-at-a-local-restaurant", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_sample-omani-cuisine-at-a-local-restaurant.jpg" + }, + { + "name": "Visit the Royal Opera House Muscat", + "description": "Immerse yourself in the world of arts and culture at the Royal Opera House Muscat. Attend a captivating performance, ranging from operas and ballets to Arabic and international music concerts. Admire the architectural grandeur of this iconic landmark and enjoy a sophisticated evening out.", + "locationName": "Muscat", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "oman", + "ref": "visit-the-royal-opera-house-muscat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_visit-the-royal-opera-house-muscat.jpg" + }, + { + "name": "Explore the Ancient City of Qalhat", + "description": "Step back in time as you explore the UNESCO World Heritage Site of Qalhat. Wander through the ruins of this once-thriving port city, including the Bibi Maryam Mausoleum and the remnants of mosques, houses, and markets. Discover the rich history and cultural significance of Qalhat, a testament to Oman's maritime past.", + "locationName": "Qalhat", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "oman", + "ref": "explore-the-ancient-city-of-qalhat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/oman_explore-the-ancient-city-of-qalhat.jpg" + }, + { + "name": "Snorkeling with Sea Lions at Isla Lobos", + "description": "Embark on a snorkeling adventure around Isla Lobos, known for its playful sea lion colony. Witness these graceful creatures underwater as they twirl and glide around you, creating an unforgettable experience.", + "locationName": "Isla Lobos", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "pagos-islands", + "ref": "snorkeling-with-sea-lions-at-isla-lobos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_snorkeling-with-sea-lions-at-isla-lobos.jpg" + }, + { + "name": "Giant Tortoise Encounter at El Chato Reserve", + "description": "Venture into the lush highlands of Santa Cruz Island to El Chato Reserve. Observe giant tortoises in their natural habitat, grazing on vegetation and lumbering across the volcanic landscape. Learn about their conservation efforts and unique life cycle.", + "locationName": "El Chato Reserve", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "pagos-islands", + "ref": "giant-tortoise-encounter-at-el-chato-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_giant-tortoise-encounter-at-el-chato-reserve.jpg" + }, + { + "name": "Hiking and Volcano Exploration on Bartolomé Island", + "description": "Embark on a scenic hike up the volcanic cone of Bartolomé Island. Be rewarded with breathtaking panoramic views of the surrounding islands and iconic Pinnacle Rock. Explore the volcanic formations and learn about the geological history of the Galápagos.", + "locationName": "Bartolomé Island", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "pagos-islands", + "ref": "hiking-and-volcano-exploration-on-bartolom-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_hiking-and-volcano-exploration-on-bartolom-island.jpg" + }, + { + "name": "Kayaking and Wildlife Spotting in Tortuga Bay", + "description": "Paddle through the calm turquoise waters of Tortuga Bay, a haven for marine life. Spot marine iguanas basking on the shores, sea turtles gliding through the water, and a variety of bird species soaring overhead.", + "locationName": "Tortuga Bay", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "pagos-islands", + "ref": "kayaking-and-wildlife-spotting-in-tortuga-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_kayaking-and-wildlife-spotting-in-tortuga-bay.jpg" + }, + { + "name": "Sunset Cruise and Stargazing", + "description": "Sail into the golden hour aboard a catamaran, enjoying stunning views of the archipelago as the sun dips below the horizon. As darkness falls, marvel at the brilliance of the unpolluted night sky, perfect for stargazing and spotting constellations.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "pagos-islands", + "ref": "sunset-cruise-and-stargazing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_sunset-cruise-and-stargazing.jpg" + }, + { + "name": "Panga Ride and Birdwatching in the Wetlands", + "description": "Embark on a relaxing panga ride through the serene wetlands of Isabela or Santa Cruz Island. Observe the fascinating avian life of the Galápagos, including flamingos, herons, and the unique flightless cormorant. Knowledgeable guides will share insights into the ecosystem and behaviors of these remarkable birds.", + "locationName": "Isabela Island or Santa Cruz Island", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "pagos-islands", + "ref": "panga-ride-and-birdwatching-in-the-wetlands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_panga-ride-and-birdwatching-in-the-wetlands.jpg" + }, + { + "name": "Biking Adventure on Floreana Island", + "description": "Explore the captivating landscapes of Floreana Island on a guided bike tour. Pedal through volcanic craters, lush highlands, and along the scenic coastline, encountering unique flora and fauna along the way. Discover hidden beaches, historical sites, and breathtaking viewpoints, all while enjoying the fresh air and exercise.", + "locationName": "Floreana Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "pagos-islands", + "ref": "biking-adventure-on-floreana-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_biking-adventure-on-floreana-island.jpg" + }, + { + "name": "Visit the Charles Darwin Research Station", + "description": "Delve into the scientific legacy of the Galápagos Islands at the Charles Darwin Research Station on Santa Cruz Island. Learn about the ongoing conservation efforts, observe the famous giant tortoises in captivity, and gain a deeper understanding of the archipelago's unique biodiversity and its importance to evolutionary science.", + "locationName": "Santa Cruz Island", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "pagos-islands", + "ref": "visit-the-charles-darwin-research-station", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_visit-the-charles-darwin-research-station.jpg" + }, + { + "name": "Surf's Up at Punta Carola", + "description": "Catch some waves at Punta Carola on San Cristobal Island, a renowned surfing spot in the Galápagos. Whether you're a seasoned surfer or a beginner, enjoy the thrill of riding the Pacific Ocean swells amidst stunning volcanic scenery. Equipment rentals and lessons are available for all skill levels.", + "locationName": "San Cristobal Island", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "pagos-islands", + "ref": "surf-s-up-at-punta-carola", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_surf-s-up-at-punta-carola.jpg" + }, + { + "name": "Savor the Flavors of Ecuadorian Cuisine", + "description": "Embark on a culinary journey through the local flavors of the Galápagos Islands. Sample fresh seafood dishes, traditional Ecuadorian cuisine, and exotic tropical fruits. Enjoy a memorable dining experience at a charming restaurant with ocean views or join a cooking class to learn the secrets of Galápagos gastronomy.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "pagos-islands", + "ref": "savor-the-flavors-of-ecuadorian-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_savor-the-flavors-of-ecuadorian-cuisine.jpg" + }, + { + "name": "Horseback Riding in the Highlands", + "description": "Embark on a scenic horseback riding adventure through the highlands of Santa Cruz Island. Explore lush landscapes, volcanic craters, and hidden trails while enjoying panoramic views of the surrounding islands. Connect with nature and experience the unique flora and fauna of the Galápagos in a tranquil and unforgettable way.", + "locationName": "Santa Cruz Island Highlands", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "pagos-islands", + "ref": "horseback-riding-in-the-highlands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_horseback-riding-in-the-highlands.jpg" + }, + { + "name": "Isabela Island Volcano Hike and Lava Tunnels", + "description": "Challenge yourself with an exhilarating hike up one of the five volcanoes on Isabela Island, the largest island in the archipelago. Witness stunning volcanic landscapes, lava fields, and unique geological formations. Descend into the mysterious lava tunnels and explore the hidden underworld of the Galápagos.", + "locationName": "Isabela Island", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "pagos-islands", + "ref": "isabela-island-volcano-hike-and-lava-tunnels", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_isabela-island-volcano-hike-and-lava-tunnels.jpg" + }, + { + "name": "Galápagos Photography Tour", + "description": "Join a specialized photography tour designed for capturing the breathtaking beauty of the Galápagos Islands. Accompanied by an expert guide and professional photographer, learn techniques for photographing unique wildlife, landscapes, and seascapes. Enhance your skills and create lasting memories through stunning photographs.", + "locationName": "Various Islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "pagos-islands", + "ref": "gal-pagos-photography-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_gal-pagos-photography-tour.jpg" + }, + { + "name": "Dive with Hammerhead Sharks at Gordon Rocks", + "description": "For the ultimate underwater thrill, embark on a scuba diving excursion to Gordon Rocks, a renowned dive site. Encounter schools of hammerhead sharks, along with other pelagic species like rays, turtles, and reef fish. Immerse yourself in the vibrant marine ecosystem and witness the awe-inspiring predators of the deep.", + "locationName": "Gordon Rocks", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "pagos-islands", + "ref": "dive-with-hammerhead-sharks-at-gordon-rocks", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_dive-with-hammerhead-sharks-at-gordon-rocks.jpg" + }, + { + "name": "Relaxation and Spa Day", + "description": "Indulge in a day of relaxation and rejuvenation at a luxurious spa on one of the main islands. Choose from a variety of treatments, including massages, facials, and body wraps, inspired by local ingredients and techniques. Unwind amidst serene surroundings and let the stresses of everyday life melt away.", + "locationName": "Santa Cruz or San Cristobal Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "pagos-islands", + "ref": "relaxation-and-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_relaxation-and-spa-day.jpg" + }, + { + "name": "Island Hopping Adventure", + "description": "Embark on a multi-day island hopping tour to experience the diverse landscapes and wildlife of the Galápagos. Each island offers unique encounters, from the volcanic formations of Isabela to the playful sea lions of San Cristobal. Explore hidden coves, hike to panoramic viewpoints, and discover the incredible biodiversity that makes this archipelago so special.", + "locationName": "Various Islands", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "pagos-islands", + "ref": "island-hopping-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_island-hopping-adventure.jpg" + }, + { + "name": "Penguins and Marine Life Boat Tour", + "description": "Join a guided boat tour around the islands to spot the adorable Galápagos penguins, the only penguin species found north of the equator. Along the way, keep an eye out for other fascinating marine creatures such as sea turtles, dolphins, and rays as they glide through the crystal-clear waters.", + "locationName": "Various Islands", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "pagos-islands", + "ref": "penguins-and-marine-life-boat-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_penguins-and-marine-life-boat-tour.jpg" + }, + { + "name": "Visit the Interpretation Centers", + "description": "Delve deeper into the unique ecosystem and history of the Galápagos Islands by visiting the interpretation centers on different islands. Learn about the volcanic origins of the archipelago, the ongoing conservation efforts, and the fascinating adaptations of the native species.", + "locationName": "Santa Cruz Island, San Cristobal Island", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "pagos-islands", + "ref": "visit-the-interpretation-centers", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_visit-the-interpretation-centers.jpg" + }, + { + "name": "Stargazing on a Remote Beach", + "description": "Escape the light pollution and experience the magic of the Galápagos night sky. Find a secluded beach, lay back on the soft sand, and marvel at the brilliance of the Milky Way and constellations, far from the distractions of city life.", + "locationName": "Various beaches", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "pagos-islands", + "ref": "stargazing-on-a-remote-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_stargazing-on-a-remote-beach.jpg" + }, + { + "name": "Indulge in Local Seafood", + "description": "Treat your taste buds to the freshest seafood delicacies the Galápagos has to offer. From succulent lobster and ceviche to grilled fish and seafood stews, the local restaurants showcase the flavors of the islands with a focus on sustainability and local ingredients.", + "locationName": "Puerto Ayora, Puerto Baquerizo Moreno", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "pagos-islands", + "ref": "indulge-in-local-seafood", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/pagos-islands_indulge-in-local-seafood.jpg" + }, + { + "name": "Trekking in Torres del Paine National Park", + "description": "Embark on a multi-day trek through the stunning landscapes of Torres del Paine National Park. Hike past towering granite peaks, turquoise lakes, and sprawling glaciers, immersing yourself in the raw beauty of Patagonia. Choose from various trails, like the famous W Trek or the challenging O Circuit, and witness breathtaking views at every turn. ", + "locationName": "Torres del Paine National Park", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "patagonia", + "ref": "trekking-in-torres-del-paine-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_trekking-in-torres-del-paine-national-park.jpg" + }, + { + "name": "Kayaking on Lago Argentino", + "description": "Paddle through the pristine waters of Lago Argentino, the largest lake in Argentina, and marvel at the majestic Perito Moreno Glacier up close. Kayak amidst icebergs of various shapes and sizes, feeling the crisp Patagonian air and enjoying the tranquility of the surrounding nature.", + "locationName": "Lago Argentino", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "patagonia", + "ref": "kayaking-on-lago-argentino", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_kayaking-on-lago-argentino.jpg" + }, + { + "name": "Wildlife Watching in Peninsula Valdés", + "description": "Explore the Peninsula Valdés, a UNESCO World Heritage Site, and encounter an array of fascinating wildlife. Spot playful sea lions, elephant seals basking on the beaches, and Magellanic penguins waddling along the shores. During the right season, you might even witness the awe-inspiring spectacle of southern right whales breaching in the ocean.", + "locationName": "Peninsula Valdés", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "patagonia", + "ref": "wildlife-watching-in-peninsula-vald-s", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_wildlife-watching-in-peninsula-vald-s.jpg" + }, + { + "name": "Horseback Riding through the Pampas", + "description": "Experience the gaucho lifestyle with a horseback riding adventure through the vast Patagonian pampas. Gallop across the plains, feeling the wind in your hair and the freedom of the open space. Learn about the gaucho culture, their traditions, and their unique horsemanship skills.", + "locationName": "Patagonian Pampas", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "patagonia", + "ref": "horseback-riding-through-the-pampas", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_horseback-riding-through-the-pampas.jpg" + }, + { + "name": "Glacier Hiking on Perito Moreno", + "description": "Embark on a once-in-a-lifetime adventure by hiking on the surface of the Perito Moreno Glacier. Strap on crampons and explore the icy wonderland, discovering crevasses, ice caves, and stunning blue pools. Witness the immense power and beauty of this natural wonder up close.", + "locationName": "Perito Moreno Glacier", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "patagonia", + "ref": "glacier-hiking-on-perito-moreno", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_glacier-hiking-on-perito-moreno.jpg" + }, + { + "name": "Stargazing in Elqui Valley", + "description": "Experience the magic of the southern hemisphere's night sky in the Elqui Valley, known as a stargazing paradise. Join a guided tour at one of the many observatories, where powerful telescopes unveil constellations, planets, and the Milky Way in breathtaking detail. Learn about the cosmos from knowledgeable guides and capture stunning astrophotography shots.", + "locationName": "Elqui Valley", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "patagonia", + "ref": "stargazing-in-elqui-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_stargazing-in-elqui-valley.jpg" + }, + { + "name": "Fly Fishing in Patagonia's Rivers", + "description": "Cast your line amidst the pristine rivers and lakes of Patagonia, a haven for fly fishing enthusiasts. With expert guides, embark on a thrilling adventure targeting brown, rainbow, and brook trout. Immerse yourself in the stunning landscapes and enjoy the tranquility of nature while testing your angling skills.", + "locationName": "Various rivers and lakes", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "patagonia", + "ref": "fly-fishing-in-patagonia-s-rivers", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_fly-fishing-in-patagonia-s-rivers.jpg" + }, + { + "name": "Wine Tasting in the Argentine Wine Region", + "description": "Indulge in the flavors of Argentina's renowned wine region, exploring vineyards nestled amidst breathtaking scenery. Embark on a guided tour and tasting, savoring Malbecs, Cabernet Sauvignons, and other varietals. Learn about the winemaking process, from grape to bottle, and appreciate the unique terroir of Patagonia.", + "locationName": "Mendoza or other wine regions", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "patagonia", + "ref": "wine-tasting-in-the-argentine-wine-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_wine-tasting-in-the-argentine-wine-region.jpg" + }, + { + "name": "Mountain Biking through Patagonia's Trails", + "description": "Embark on an exhilarating mountain biking adventure through Patagonia's diverse landscapes. Explore scenic trails that wind through forests, mountains, and valleys, offering breathtaking views and challenging terrain. Whether you're a seasoned rider or a beginner, there are trails suitable for all levels, providing an unforgettable way to experience the region's natural beauty.", + "locationName": "Various mountain biking trails", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "patagonia", + "ref": "mountain-biking-through-patagonia-s-trails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_mountain-biking-through-patagonia-s-trails.jpg" + }, + { + "name": "Exploring the Culture of Indigenous Communities", + "description": "Immerse yourself in the rich cultural heritage of Patagonia's indigenous communities, such as the Mapuche. Visit local villages, interact with residents, and learn about their traditions, crafts, and way of life. Participate in cultural events, workshops, or guided tours to gain a deeper understanding of their connection to the land and their enduring traditions.", + "locationName": "Various indigenous communities", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "patagonia", + "ref": "exploring-the-culture-of-indigenous-communities", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_exploring-the-culture-of-indigenous-communities.jpg" + }, + { + "name": "Scenic Flight Over the Andes", + "description": "Embark on an unforgettable aerial adventure with a scenic flight over the majestic Andes Mountains. Witness the breathtaking panorama of snow-capped peaks, glaciers, and turquoise lakes, capturing stunning aerial photography. This experience offers a unique perspective of Patagonia's vast and awe-inspiring landscapes.", + "locationName": "Various locations throughout Patagonia", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "patagonia", + "ref": "scenic-flight-over-the-andes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_scenic-flight-over-the-andes.jpg" + }, + { + "name": "Whale Watching in Puerto Madryn", + "description": "Embark on a thrilling whale-watching excursion in Puerto Madryn, a prime location for observing Southern Right Whales. Witness these magnificent creatures up close as they breach, tail slap, and nurse their calves in the protected waters of Golfo Nuevo. This unforgettable experience offers a unique opportunity to connect with Patagonia's marine wildlife.", + "locationName": "Puerto Madryn", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "patagonia", + "ref": "whale-watching-in-puerto-madryn", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_whale-watching-in-puerto-madryn.jpg" + }, + { + "name": "4x4 Off-Road Adventure", + "description": "Experience the rugged beauty of Patagonia on an exhilarating 4x4 off-road adventure. Traverse challenging terrains, cross glacial rivers, and discover hidden valleys and remote landscapes inaccessible by conventional vehicles. This adrenaline-pumping activity offers a thrilling way to explore Patagonia's wild and untamed beauty.", + "locationName": "Various locations throughout Patagonia", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "patagonia", + "ref": "4x4-off-road-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_4x4-off-road-adventure.jpg" + }, + { + "name": "Relaxing at a Patagonian Estancia", + "description": "Immerse yourself in the authentic Patagonian lifestyle with a stay at a traditional estancia (ranch). Enjoy horseback riding, sheep shearing demonstrations, and delicious Argentine asados (barbecues). Experience the tranquility of rural life and connect with the warm hospitality of local gauchos (cowboys).", + "locationName": "Various locations throughout Patagonia", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "patagonia", + "ref": "relaxing-at-a-patagonian-estancia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_relaxing-at-a-patagonian-estancia.jpg" + }, + { + "name": "Exploring the Caves of Patagonia", + "description": "Discover the hidden wonders beneath the surface with a spelunking adventure in Patagonia's caves. Explore ancient formations, stalactites, and stalagmites, and learn about the geological history of the region. This unique activity offers a different perspective of Patagonia's natural beauty and is perfect for adventurous travelers.", + "locationName": "Various locations throughout Patagonia", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "patagonia", + "ref": "exploring-the-caves-of-patagonia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_exploring-the-caves-of-patagonia.jpg" + }, + { + "name": "Sailing in the Beagle Channel", + "description": "Embark on a scenic boat tour through the Beagle Channel, a strait in the Tierra del Fuego Archipelago, and marvel at the stunning landscapes of snow-capped mountains, glaciers, and lush forests. Spot diverse wildlife including sea lions, penguins, and various bird species. Choose from various tour options, ranging from half-day excursions to multi-day expeditions, and experience the unique beauty of this remote waterway.", + "locationName": "Ushuaia or Puerto Williams", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "patagonia", + "ref": "sailing-in-the-beagle-channel", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_sailing-in-the-beagle-channel.jpg" + }, + { + "name": "Birdwatching in Punta Tombo", + "description": "Visit Punta Tombo, a nature reserve on the Atlantic coast, and witness one of the largest Magellanic penguin colonies in the world. Observe these adorable creatures up close as they waddle, swim, and nest along the shore. The reserve also hosts other bird species like cormorants, gulls, and rheas, making it a paradise for birdwatchers and nature enthusiasts.", + "locationName": "Punta Tombo", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "patagonia", + "ref": "birdwatching-in-punta-tombo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_birdwatching-in-punta-tombo.jpg" + }, + { + "name": "White-Water Rafting on the Futaleufú River", + "description": "Experience an adrenaline-pumping adventure with a white-water rafting trip on the Futaleufú River. Navigate through thrilling rapids surrounded by breathtaking scenery of mountains, canyons, and forests. Choose from various difficulty levels and enjoy a day of excitement and stunning natural beauty. ", + "locationName": "Futaleufú River", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "patagonia", + "ref": "white-water-rafting-on-the-futaleuf-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_white-water-rafting-on-the-futaleuf-river.jpg" + }, + { + "name": "Skiing or Snowboarding in Cerro Catedral", + "description": "Hit the slopes at Cerro Catedral, one of the largest ski resorts in South America. Enjoy a variety of trails for all skill levels, from gentle beginner slopes to challenging black diamond runs. Take in the panoramic views of the Andes mountains as you carve down the snow-covered slopes. The resort also offers other winter activities like snowshoeing and cross-country skiing.", + "locationName": "Cerro Catedral", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "patagonia", + "ref": "skiing-or-snowboarding-in-cerro-catedral", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_skiing-or-snowboarding-in-cerro-catedral.jpg" + }, + { + "name": "Visiting the Perito Moreno Glacier", + "description": "Witness the awe-inspiring Perito Moreno Glacier, a massive ice formation in Los Glaciares National Park. Take a boat tour to get up close to the glacier and marvel at its towering ice walls and deep blue crevasses. You might even witness the spectacular sight of ice calving, where large chunks of ice break off and crash into the water. For a more adventurous experience, opt for a glacier trekking tour and walk on the surface of this natural wonder.", + "locationName": "Los Glaciares National Park", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "patagonia", + "ref": "visiting-the-perito-moreno-glacier", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/patagonia_visiting-the-perito-moreno-glacier.jpg" + }, + { + "name": "Explore the Historic Center", + "description": "Wander through the UNESCO-listed Historic Center of Arequipa, admiring the stunning colonial architecture made of white volcanic stone. Visit the Plaza de Armas, the main square, and marvel at the intricate carvings of the Arequipa Cathedral. Discover hidden courtyards, charming cafes, and local shops as you soak in the city's rich history.", + "locationName": "Historic Center of Arequipa", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "peru", + "ref": "explore-the-historic-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_explore-the-historic-center.jpg" + }, + { + "name": "Visit the Santa Catalina Monastery", + "description": "Step back in time at the Santa Catalina Monastery, a 16th-century complex that was once home to cloistered nuns. Explore the colorful streets, peaceful gardens, and hidden chapels of this city within a city. Learn about the fascinating lives of the nuns and admire the religious art and architecture.", + "locationName": "Santa Catalina Monastery", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "peru", + "ref": "visit-the-santa-catalina-monastery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_visit-the-santa-catalina-monastery.jpg" + }, + { + "name": "Hike the Colca Canyon", + "description": "Embark on a breathtaking hike through the Colca Canyon, one of the deepest canyons in the world. Witness stunning landscapes, traditional Andean villages, and soaring condors. Choose from various hiking trails, ranging from easy to challenging, and enjoy panoramic views of the canyon and surrounding mountains.", + "locationName": "Colca Canyon", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "peru", + "ref": "hike-the-colca-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_hike-the-colca-canyon.jpg" + }, + { + "name": "Savor Peruvian Cuisine", + "description": "Indulge in the delicious flavors of Peruvian cuisine at Arequipa's many restaurants and cafes. Sample local specialties such as rocoto relleno (stuffed spicy peppers), chupe de camarones (shrimp chowder), and adobo arequipeño (marinated pork dish). Don't forget to try the traditional drink, chicha de jora, made from fermented corn.", + "locationName": "Various restaurants and cafes", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "peru", + "ref": "savor-peruvian-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_savor-peruvian-cuisine.jpg" + }, + { + "name": "Yanahuara Viewpoint", + "description": "Capture stunning panoramic views of the city and the surrounding volcanoes from the Yanahuara Viewpoint. Admire the white-stone arches and enjoy a peaceful moment while taking in the breathtaking scenery. This is a perfect spot for photography enthusiasts and anyone who wants to appreciate the beauty of Arequipa.", + "locationName": "Yanahuara Viewpoint", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "peru", + "ref": "yanahuara-viewpoint", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_yanahuara-viewpoint.jpg" + }, + { + "name": "Whitewater Rafting on the Chili River", + "description": "Experience the thrill of whitewater rafting on the Chili River, which flows through the Arequipa region. Choose from different difficulty levels depending on your experience and enjoy breathtaking views of the surrounding canyons and landscapes. This is a perfect activity for adventure seekers and nature lovers.", + "locationName": "Chili River", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "peru", + "ref": "whitewater-rafting-on-the-chili-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_whitewater-rafting-on-the-chili-river.jpg" + }, + { + "name": "Stargazing in the Desert", + "description": "Escape the city lights and embark on a stargazing adventure in the Atacama Desert, one of the driest deserts in the world. With clear skies and minimal light pollution, you'll have an unforgettable experience observing constellations, planets, and even the Milky Way.", + "locationName": "Atacama Desert", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "peru", + "ref": "stargazing-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_stargazing-in-the-desert.jpg" + }, + { + "name": "Soak in the Thermal Baths of Yura", + "description": "Relax and rejuvenate in the natural thermal baths of Yura, located just outside of Arequipa. The mineral-rich waters are known for their therapeutic properties and offer a perfect escape from the city's hustle and bustle.", + "locationName": "Yura", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "peru", + "ref": "soak-in-the-thermal-baths-of-yura", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_soak-in-the-thermal-baths-of-yura.jpg" + }, + { + "name": "Visit the Mundo Alpaca", + "description": "Learn about the fascinating world of alpacas at Mundo Alpaca, a farm and textile center. Witness the shearing process, discover the different types of alpaca wool, and shop for unique handcrafted souvenirs made from this luxurious fiber.", + "locationName": "Mundo Alpaca", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "peru", + "ref": "visit-the-mundo-alpaca", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_visit-the-mundo-alpaca.jpg" + }, + { + "name": "Explore the Arequipa Countryside on Horseback", + "description": "Embark on a scenic horseback riding tour through the picturesque countryside surrounding Arequipa. Admire the stunning views of the volcanoes, valleys, and traditional villages, and experience the local way of life.", + "locationName": "Arequipa Countryside", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "peru", + "ref": "explore-the-arequipa-countryside-on-horseback", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_explore-the-arequipa-countryside-on-horseback.jpg" + }, + { + "name": "Rock Climbing and Bouldering", + "description": "Challenge yourself with the volcanic cliffs surrounding Arequipa. Several outfitters offer guided climbing and bouldering excursions for all skill levels, from beginners to seasoned climbers. Experience the thrill of reaching new heights while enjoying panoramic views of the city and surrounding landscapes.", + "locationName": "Quebrada de Culebrillas or Charcani", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "peru", + "ref": "rock-climbing-and-bouldering", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_rock-climbing-and-bouldering.jpg" + }, + { + "name": "Mountain Biking Adventure", + "description": "Explore the rugged terrain around Arequipa on a thrilling mountain biking adventure. Several routes cater to different skill levels, offering stunning views of the Andes Mountains and the Colca Valley. You can choose a guided tour or rent a bike and explore independently.", + "locationName": "Yura or Chilina Valley", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "peru", + "ref": "mountain-biking-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_mountain-biking-adventure.jpg" + }, + { + "name": "Cooking Class and Food Market Tour", + "description": "Delve into the vibrant culinary scene of Arequipa with a cooking class and food market tour. Learn to prepare traditional Peruvian dishes like rocoto relleno or ocopa, using fresh local ingredients. Visit a bustling market to discover the variety of fruits, vegetables, and spices that contribute to the region's unique flavors.", + "locationName": "San Camilo Market or local cooking schools", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "peru", + "ref": "cooking-class-and-food-market-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_cooking-class-and-food-market-tour.jpg" + }, + { + "name": "Sillar Route Tour", + "description": "Discover the source of Arequipa's white volcanic stone on a Sillar Route tour. Visit the quarries where sillar is extracted and learn about its use in the city's iconic architecture. Explore the unique landscapes formed by volcanic activity and gain insight into the geological history of the region.", + "locationName": "Añashuayco or Quebrada de Añashuayco", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "peru", + "ref": "sillar-route-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_sillar-route-tour.jpg" + }, + { + "name": "Picanterias Food Tour", + "description": "Embark on a culinary journey through Arequipa's traditional picanterias. These family-run restaurants offer a unique dining experience, serving up hearty and flavorful dishes in a lively atmosphere. Sample local specialties like chupe de camarones, adobo arequipeño, and pastel de papa.", + "locationName": "Yanahuara or Sachaca districts", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "peru", + "ref": "picanterias-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_picanterias-food-tour.jpg" + }, + { + "name": "Sandboarding and Dune Buggying in the Desert", + "description": "Experience the thrill of gliding down the sand dunes on a sandboard or riding a dune buggy across the vast desert landscape. The surrounding desert offers exhilarating adventures for those seeking an adrenaline rush.", + "locationName": "Arequipa Desert", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "peru", + "ref": "sandboarding-and-dune-buggying-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_sandboarding-and-dune-buggying-in-the-desert.jpg" + }, + { + "name": "Catch a Show at the Teatro Municipal", + "description": "Immerse yourself in the local arts and culture scene by attending a performance at the Teatro Municipal. Enjoy a variety of shows, including music concerts, dance performances, and theatrical productions.", + "locationName": "Teatro Municipal de Arequipa", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "peru", + "ref": "catch-a-show-at-the-teatro-municipal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_catch-a-show-at-the-teatro-municipal.jpg" + }, + { + "name": "Explore the San Camilo Market", + "description": "Wander through the vibrant San Camilo Market, a bustling hub of local life. Discover an array of fresh produce, handcrafted souvenirs, traditional textiles, and unique Peruvian delicacies.", + "locationName": "San Camilo Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "peru", + "ref": "explore-the-san-camilo-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_explore-the-san-camilo-market.jpg" + }, + { + "name": "Take a Day Trip to the Salinas and Aguada Blanca National Reserve", + "description": "Embark on a day trip to the Salinas and Aguada Blanca National Reserve, a stunning natural area home to diverse wildlife, including vicuñas, alpacas, and flamingos. Explore the breathtaking landscapes, salt flats, and volcanic formations.", + "locationName": "Salinas and Aguada Blanca National Reserve", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "peru", + "ref": "take-a-day-trip-to-the-salinas-and-aguada-blanca-national-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peru_take-a-day-trip-to-the-salinas-and-aguada-blanca-national-reserve.jpg" + }, + { + "name": "Amazon River Cruise", + "description": "Embark on a scenic river cruise along the Amazon River, the lifeblood of the rainforest. Spot diverse wildlife such as pink river dolphins, caimans, monkeys, and exotic birds while enjoying the lush scenery and the sounds of the jungle. Opt for a sunrise or sunset cruise for a truly magical experience.", + "locationName": "Amazon River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "peruvian-amazon", + "ref": "amazon-river-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_amazon-river-cruise.jpg" + }, + { + "name": "Jungle Trekking and Wildlife Watching", + "description": "Venture deep into the rainforest on a guided jungle trek, immersing yourself in the sights and sounds of the Amazon. Learn about medicinal plants, observe fascinating insects, and encounter unique wildlife like sloths, monkeys, and colorful birds. Keep an eye out for elusive creatures like jaguars and tapirs.", + "locationName": "Various jungle trails", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "peruvian-amazon", + "ref": "jungle-trekking-and-wildlife-watching", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_jungle-trekking-and-wildlife-watching.jpg" + }, + { + "name": "Canopy Walkway Adventure", + "description": "Experience the rainforest from a different perspective by taking a thrilling canopy walkway tour. Walk among the treetops, enjoying breathtaking views of the jungle canopy and observing the rich biodiversity from above. This is a fantastic opportunity to spot birds, monkeys, and other arboreal creatures.", + "locationName": "Various canopy walkway locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "peruvian-amazon", + "ref": "canopy-walkway-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_canopy-walkway-adventure.jpg" + }, + { + "name": "Visit an Indigenous Community", + "description": "Immerse yourself in the culture of the indigenous people of the Amazon by visiting a local community. Learn about their traditions, way of life, and connection to the rainforest. Participate in cultural activities, try traditional foods, and gain a deeper understanding of the human history of the Amazon.", + "locationName": "Various indigenous communities", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "peruvian-amazon", + "ref": "visit-an-indigenous-community", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_visit-an-indigenous-community.jpg" + }, + { + "name": "Nighttime Wildlife Safari", + "description": "Embark on a thrilling nighttime safari into the rainforest, where nocturnal creatures come alive. With the help of a guide, spot caimans, snakes, owls, and other fascinating animals that are active after dark. This is a unique opportunity to experience the magic of the Amazon at night.", + "locationName": "Various jungle locations", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "peruvian-amazon", + "ref": "nighttime-wildlife-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_nighttime-wildlife-safari.jpg" + }, + { + "name": "Piranha Fishing Excursion", + "description": "Experience the thrill of piranha fishing in the Amazon River. Local guides will teach you traditional fishing techniques and share insights about these fascinating creatures. Enjoy the excitement of reeling in your catch and learn about the important role piranhas play in the ecosystem.", + "locationName": "Amazon River tributaries", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "peruvian-amazon", + "ref": "piranha-fishing-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_piranha-fishing-excursion.jpg" + }, + { + "name": "Kayaking through Amazonian Waterways", + "description": "Embark on a serene kayaking adventure through the tranquil waterways of the Amazon. Paddle through flooded forests, observe diverse wildlife along the riverbanks, and immerse yourself in the peaceful beauty of the rainforest. This activity offers a unique perspective of the Amazon's ecosystem and allows you to connect with nature at your own pace.", + "locationName": "Amazon River tributaries and flooded forests", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "peruvian-amazon", + "ref": "kayaking-through-amazonian-waterways", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_kayaking-through-amazonian-waterways.jpg" + }, + { + "name": "Birdwatching in the Rainforest Canopy", + "description": "Join a guided birdwatching tour and witness the incredible diversity of avian life in the Amazon rainforest. With the help of experienced local guides, spot colorful macaws, toucans, hummingbirds, and numerous other bird species. Learn about their unique behaviors, calls, and the vital role they play in the rainforest ecosystem. This activity is perfect for nature enthusiasts and photography lovers.", + "locationName": "Various locations within the rainforest", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "peruvian-amazon", + "ref": "birdwatching-in-the-rainforest-canopy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_birdwatching-in-the-rainforest-canopy.jpg" + }, + { + "name": "Learn Traditional Crafts with Indigenous Artisans", + "description": "Immerse yourself in the rich cultural heritage of the Amazon by participating in a workshop with indigenous artisans. Learn traditional crafts such as basket weaving, pottery making, or jewelry crafting using natural materials found in the rainforest. This experience provides a unique opportunity to connect with local communities, support their livelihoods, and gain a deeper appreciation for their artistic traditions.", + "locationName": "Indigenous villages or cultural centers", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "peruvian-amazon", + "ref": "learn-traditional-crafts-with-indigenous-artisans", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_learn-traditional-crafts-with-indigenous-artisans.jpg" + }, + { + "name": "Relaxing Hammock Retreat and Nature Sounds", + "description": "Escape the hustle and bustle of everyday life with a relaxing hammock retreat in the heart of the Amazon rainforest. Find a peaceful spot, sway gently in a hammock, and let the soothing sounds of nature wash over you. Listen to the calls of exotic birds, the rustling of leaves, and the gentle flow of nearby rivers. This activity is perfect for those seeking tranquility and a chance to reconnect with nature.", + "locationName": "Various lodges or jungle camps", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "peruvian-amazon", + "ref": "relaxing-hammock-retreat-and-nature-sounds", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_relaxing-hammock-retreat-and-nature-sounds.jpg" + }, + { + "name": "Stargazing in the Amazonian Night", + "description": "Escape the city lights and experience the magic of the Amazonian night sky. Join a local guide for an evening of stargazing, where you'll learn about constellations, planets, and the fascinating stories behind them. With minimal light pollution, the Amazon offers breathtaking views of the Milky Way and countless stars, creating an unforgettable celestial experience.", + "locationName": "Various jungle lodges and camps", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "peruvian-amazon", + "ref": "stargazing-in-the-amazonian-night", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_stargazing-in-the-amazonian-night.jpg" + }, + { + "name": "Swim with Pink River Dolphins", + "description": "Embark on a unique adventure to swim with the legendary pink river dolphins of the Amazon. These intelligent and playful creatures are native to the region and offer a truly unforgettable experience. Take a dip in the river alongside these gentle giants and learn about their importance in the Amazonian ecosystem.", + "locationName": "Amazon River tributaries", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "peruvian-amazon", + "ref": "swim-with-pink-river-dolphins", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_swim-with-pink-river-dolphins.jpg" + }, + { + "name": "Photography Expedition in the Rainforest", + "description": "Capture the beauty and biodiversity of the Amazon rainforest on a photography expedition. Led by experienced guides and local photographers, you'll explore diverse ecosystems, learn about unique flora and fauna, and receive expert tips on capturing stunning images of the rainforest's hidden wonders. This activity is perfect for photography enthusiasts of all levels.", + "locationName": "Various rainforest locations", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "peruvian-amazon", + "ref": "photography-expedition-in-the-rainforest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_photography-expedition-in-the-rainforest.jpg" + }, + { + "name": "Medicinal Plant Walk and Workshop", + "description": "Discover the healing power of the Amazon rainforest on a medicinal plant walk and workshop. Learn from indigenous healers about traditional uses of plants for various ailments and wellness practices. Participate in a hands-on workshop to create your own natural remedies and gain insights into the rich medicinal knowledge of the Amazonian people.", + "locationName": "Indigenous communities or jungle lodges", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "peruvian-amazon", + "ref": "medicinal-plant-walk-and-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_medicinal-plant-walk-and-workshop.jpg" + }, + { + "name": "Amazonian Cooking Class", + "description": "Delve into the flavors of the Amazon with a hands-on cooking class. Learn from local chefs about traditional ingredients and cooking techniques used in Amazonian cuisine. Prepare and savor authentic dishes like juanes (rice and chicken wrapped in bijao leaves) or tacacho (plantain and pork dish) while experiencing the unique culinary heritage of the region.", + "locationName": "Jungle lodges or local communities", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "peruvian-amazon", + "ref": "amazonian-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_amazonian-cooking-class.jpg" + }, + { + "name": "Mountain Biking through the Rainforest", + "description": "Embark on an exhilarating mountain biking adventure through the lush rainforest trails. Pedal past towering trees, cascading waterfalls, and hidden creeks, feeling the thrill of the rugged terrain and the fresh air on your face. This adrenaline-pumping experience offers a unique perspective of the Amazon's diverse ecosystem and is perfect for adventure seekers.", + "locationName": "Various rainforest trails", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "peruvian-amazon", + "ref": "mountain-biking-through-the-rainforest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_mountain-biking-through-the-rainforest.jpg" + }, + { + "name": "Indigenous Village Homestay", + "description": "Immerse yourself in the rich culture and traditions of the Amazonian people by participating in a homestay program with an indigenous community. Share meals, learn traditional crafts, and experience their way of life firsthand. This unique opportunity offers a glimpse into the fascinating customs and deep connection to nature that have been passed down for generations.", + "locationName": "Indigenous villages", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "peruvian-amazon", + "ref": "indigenous-village-homestay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_indigenous-village-homestay.jpg" + }, + { + "name": "Amazon River Stand-Up Paddleboarding", + "description": "Glide along the tranquil waters of the Amazon River on a stand-up paddleboard, enjoying a peaceful and unique perspective of the rainforest. Observe the diverse wildlife along the riverbanks, from playful monkeys to colorful birds, and soak in the serenity of the surrounding nature. This relaxing activity is perfect for all skill levels and offers a chance to connect with the Amazon's aquatic ecosystem.", + "locationName": "Amazon River", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "peruvian-amazon", + "ref": "amazon-river-stand-up-paddleboarding", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_amazon-river-stand-up-paddleboarding.jpg" + }, + { + "name": "Caving Expedition", + "description": "Embark on a thrilling caving expedition, exploring the hidden depths of the Amazonian caves. Discover stunning rock formations, underground rivers, and unique cave-dwelling creatures. This adventurous activity is perfect for those seeking an adrenaline rush and a glimpse into the geological wonders of the rainforest.", + "locationName": "Various cave systems", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "peruvian-amazon", + "ref": "caving-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_caving-expedition.jpg" + }, + { + "name": "Rainforest Photography Workshop", + "description": "Capture the beauty and diversity of the Amazon rainforest through a photography workshop led by experienced guides. Learn valuable techniques for photographing wildlife, landscapes, and indigenous cultures. This workshop is perfect for photography enthusiasts of all levels and provides an opportunity to create lasting memories of your Amazonian adventure.", + "locationName": "Various rainforest locations", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "peruvian-amazon", + "ref": "rainforest-photography-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/peruvian-amazon_rainforest-photography-workshop.jpg" + }, + { + "name": "Royal Palace Complex Exploration", + "description": "Embark on a captivating journey through the Royal Palace Complex, a stunning symbol of Cambodian royalty. Marvel at the intricate architecture of the Silver Pagoda, adorned with shimmering silver tiles, and witness the grandeur of the Throne Hall. Explore the lush gardens and immerse yourself in the opulent history of the Cambodian monarchy.", + "locationName": "Royal Palace Complex", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "phnom-penh", + "ref": "royal-palace-complex-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_royal-palace-complex-exploration.jpg" + }, + { + "name": "Wat Phnom: A Historical and Spiritual Ascent", + "description": "Climb the steps to Wat Phnom, a revered Buddhist temple perched atop a hill, offering panoramic city views. Discover the legend of Lady Penh, who is said to have found four Buddha statues within a tree trunk and built the temple to house them. Immerse yourself in the serene atmosphere, observe the locals praying, and learn about the temple's significance in Cambodian culture.", + "locationName": "Wat Phnom", + "duration": 1.5, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "wat-phnom-a-historical-and-spiritual-ascent", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_wat-phnom-a-historical-and-spiritual-ascent.jpg" + }, + { + "name": "Tuol Sleng Genocide Museum: A Poignant Journey", + "description": "Pay your respects at the Tuol Sleng Genocide Museum, a former school that was converted into a prison during the Khmer Rouge regime. Gain a deeper understanding of Cambodia's tragic past as you explore the exhibits and learn about the atrocities that took place. This experience is a somber but important reminder of the resilience of the human spirit.", + "locationName": "Tuol Sleng Genocide Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "tuol-sleng-genocide-museum-a-poignant-journey", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_tuol-sleng-genocide-museum-a-poignant-journey.jpg" + }, + { + "name": "Sunset Cruise on the Mekong River", + "description": "Indulge in a relaxing sunset cruise along the Mekong River. Admire the city skyline as it transforms into a tapestry of colors, observe the local life along the riverbanks, and savor a delicious Cambodian dinner on board. This experience offers a perfect blend of sightseeing and tranquility.", + "locationName": "Mekong River", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "phnom-penh", + "ref": "sunset-cruise-on-the-mekong-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_sunset-cruise-on-the-mekong-river.jpg" + }, + { + "name": "Phsar Thmey: A Shopper's Paradise", + "description": "Explore the vibrant Phsar Thmey, also known as the Central Market, a bustling hub for shopping and cultural immersion. Browse through a diverse array of goods, including textiles, handicrafts, souvenirs, and local produce. Engage with friendly vendors, practice your bargaining skills, and discover unique treasures to take home.", + "locationName": "Phsar Thmey (Central Market)", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "phsar-thmey-a-shopper-s-paradise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_phsar-thmey-a-shopper-s-paradise.jpg" + }, + { + "name": "Koh Dach (Silk Island) Excursion", + "description": "Embark on a captivating journey to Koh Dach, also known as Silk Island, just a short ferry ride from Phnom Penh. Witness the intricate art of silk weaving passed down through generations, explore traditional stilt houses, and immerse yourself in the island's serene rural atmosphere. You can even purchase exquisite silk products directly from the local artisans.", + "locationName": "Koh Dach (Silk Island)", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "koh-dach-silk-island-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_koh-dach-silk-island-excursion.jpg" + }, + { + "name": "Savor Culinary Delights at Friends the Restaurant", + "description": "Indulge in a unique dining experience at Friends the Restaurant, a renowned training restaurant empowering marginalized youth with culinary skills. Enjoy delicious Khmer and Western dishes while supporting a worthy cause, knowing that your meal contributes to a brighter future for these aspiring chefs.", + "locationName": "Friends the Restaurant", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "phnom-penh", + "ref": "savor-culinary-delights-at-friends-the-restaurant", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_savor-culinary-delights-at-friends-the-restaurant.jpg" + }, + { + "name": "Unwind at a Traditional Khmer Spa", + "description": "Escape the bustling city and rejuvenate your senses with a traditional Khmer spa treatment. Experience the therapeutic benefits of ancient massage techniques, herbal remedies, and aromatic oils, leaving you feeling refreshed and revitalized.", + "locationName": "Various spas in Phnom Penh", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "phnom-penh", + "ref": "unwind-at-a-traditional-khmer-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_unwind-at-a-traditional-khmer-spa.jpg" + }, + { + "name": "Discover Local Treasures at the Russian Market", + "description": "Embark on a treasure hunt at the Russian Market, a vibrant labyrinth of stalls offering everything from souvenirs and handicrafts to clothing, jewelry, and antiques. Practice your bargaining skills and find unique mementos to remember your Cambodian adventure.", + "locationName": "Russian Market", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "discover-local-treasures-at-the-russian-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_discover-local-treasures-at-the-russian-market.jpg" + }, + { + "name": "Cambodian Living Arts Performance", + "description": "Immerse yourself in the rich cultural heritage of Cambodia at a mesmerizing performance by the Cambodian Living Arts. Witness traditional Apsara dance, shadow puppetry, and other captivating art forms, preserving and celebrating the country's artistic legacy.", + "locationName": "National Museum of Phnom Penh", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "phnom-penh", + "ref": "cambodian-living-arts-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_cambodian-living-arts-performance.jpg" + }, + { + "name": "Cycling the Cambodian Countryside", + "description": "Embark on a scenic cycling tour through the picturesque Cambodian countryside surrounding Phnom Penh. Pedal along tranquil rice paddies, observe local village life, and witness the beauty of rural Cambodia. This leisurely adventure offers a refreshing escape from the city's hustle and bustle, allowing you to connect with nature and experience authentic Cambodian culture.", + "locationName": "Rural areas surrounding Phnom Penh", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "cycling-the-cambodian-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_cycling-the-cambodian-countryside.jpg" + }, + { + "name": "National Museum of Cambodia: A Journey Through Khmer Art and History", + "description": "Delve into the rich cultural heritage of Cambodia at the National Museum, home to an extensive collection of Khmer art and artifacts. Explore ancient sculptures, intricate carvings, and historical relics that showcase the country's fascinating past. Gain insights into the Angkorian era, learn about traditional craftsmanship, and appreciate the beauty of Khmer artistic expression.", + "locationName": "National Museum of Cambodia", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "national-museum-of-cambodia-a-journey-through-khmer-art-and-history", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_national-museum-of-cambodia-a-journey-through-khmer-art-and-history.jpg" + }, + { + "name": "Koh Rong Island Getaway", + "description": "Escape the city and embark on a tropical island adventure to Koh Rong. Relax on pristine beaches, swim in crystal-clear waters, and explore the island's lush interior. Go snorkeling or diving to discover vibrant coral reefs teeming with marine life. Indulge in fresh seafood at beachfront restaurants and enjoy the laid-back island vibes.", + "locationName": "Koh Rong Island", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "phnom-penh", + "ref": "koh-rong-island-getaway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_koh-rong-island-getaway.jpg" + }, + { + "name": "Phnom Chisor Temple: Ancient Ruins with Panoramic Views", + "description": "Embark on a journey to Phnom Chisor, an ancient temple complex perched atop a hill, offering breathtaking panoramic views of the surrounding countryside. Climb the steps to the temple and explore the intricate carvings and architectural details. Learn about the history and significance of this sacred site while enjoying the serene atmosphere and stunning vistas.", + "locationName": "Phnom Chisor", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "phnom-chisor-temple-ancient-ruins-with-panoramic-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_phnom-chisor-temple-ancient-ruins-with-panoramic-views.jpg" + }, + { + "name": "Indulge in Street Food Delights", + "description": "Embark on a culinary adventure through the vibrant streets of Phnom Penh, sampling a variety of delicious street food offerings. From flavorful noodle soups and savory grilled meats to refreshing fruit shakes and sweet treats, the city's street food scene is a feast for the senses. Discover local favorites, experience authentic Khmer flavors, and enjoy the lively atmosphere of the city's food stalls.", + "locationName": "Various street food stalls throughout Phnom Penh", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "phnom-penh", + "ref": "indulge-in-street-food-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_indulge-in-street-food-delights.jpg" + }, + { + "name": "Killing Fields of Choeung Ek: A Somber Reflection", + "description": "Embark on a deeply moving journey to the Killing Fields of Choeung Ek, a poignant memorial dedicated to the victims of the Khmer Rouge regime. Pay your respects at the stupa filled with skulls and learn about Cambodia's tragic past. This experience offers a profound understanding of the country's history and resilience.", + "locationName": "Choeung Ek Genocidal Center", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "killing-fields-of-choeung-ek-a-somber-reflection", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_killing-fields-of-choeung-ek-a-somber-reflection.jpg" + }, + { + "name": "Wat Ounalom: Serenity Amidst the City", + "description": "Escape the bustling city and find tranquility at Wat Ounalom, one of Phnom Penh's oldest and most important Buddhist temples. Admire the intricate architecture, observe monks in prayer, and experience the serene atmosphere of this spiritual haven.", + "locationName": "Wat Ounalom", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "phnom-penh", + "ref": "wat-ounalom-serenity-amidst-the-city", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_wat-ounalom-serenity-amidst-the-city.jpg" + }, + { + "name": "Sunset Kayak on the Tonle Sap River", + "description": "Embark on a picturesque kayaking adventure along the Tonle Sap River as the sun begins its descent. Witness the city skyline bathed in golden light, observe local life along the riverbanks, and enjoy the tranquility of the water. This unique perspective offers stunning views and a memorable experience.", + "locationName": "Tonle Sap River", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "phnom-penh", + "ref": "sunset-kayak-on-the-tonle-sap-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_sunset-kayak-on-the-tonle-sap-river.jpg" + }, + { + "name": "Cambodian Cooking Class: Master Khmer Flavors", + "description": "Delve into the heart of Cambodian cuisine with a hands-on cooking class. Learn the secrets of traditional dishes like fish amok, Khmer curry, and fresh spring rolls under the guidance of a local chef. This immersive experience allows you to recreate authentic flavors and impress your friends back home.", + "locationName": "Various Cooking Schools", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "phnom-penh", + "ref": "cambodian-cooking-class-master-khmer-flavors", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_cambodian-cooking-class-master-khmer-flavors.jpg" + }, + { + "name": "Phnom Tamao Wildlife Rescue Center: A Sanctuary for Animals", + "description": "Take a day trip to the Phnom Tamao Wildlife Rescue Center, a sanctuary for rescued animals including elephants, tigers, and gibbons. Learn about conservation efforts, observe the animals in their natural habitats, and contribute to the center's mission of protecting Cambodia's wildlife.", + "locationName": "Phnom Tamao Wildlife Rescue Center", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "phnom-penh", + "ref": "phnom-tamao-wildlife-rescue-center-a-sanctuary-for-animals", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phnom-penh_phnom-tamao-wildlife-rescue-center-a-sanctuary-for-animals.jpg" + }, + { + "name": "Island Hopping and Snorkeling Adventure", + "description": "Embark on a full-day island-hopping tour to discover the beauty of Phi Phi Islands and Phang Nga Bay. Dive into crystal-clear waters for snorkeling among colorful coral reefs and tropical fish, explore hidden coves and lagoons, and relax on pristine beaches. This adventure offers a perfect blend of relaxation and exploration, making it ideal for families and nature enthusiasts.", + "locationName": "Phi Phi Islands and Phang Nga Bay", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "phuket", + "ref": "island-hopping-and-snorkeling-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_island-hopping-and-snorkeling-adventure.jpg" + }, + { + "name": "Phuket Town Night Market Exploration", + "description": "Delve into the vibrant atmosphere of Phuket Town's Sunday Walking Street Market. Wander through the bustling stalls filled with local handicrafts, souvenirs, clothing, and street food. Savor the flavors of authentic Thai cuisine, enjoy live music performances, and experience the lively ambiance of this popular night market. This is a perfect opportunity to discover local culture and indulge in some shopping.", + "locationName": "Phuket Town", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "phuket", + "ref": "phuket-town-night-market-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_phuket-town-night-market-exploration.jpg" + }, + { + "name": "Thai Cooking Class and Market Tour", + "description": "Learn the secrets of Thai cuisine with a hands-on cooking class. Start with a visit to a local market to select fresh ingredients, then master the art of preparing traditional dishes under the guidance of experienced chefs. Discover the flavors and techniques of Thai cooking, from fragrant curries to delicious stir-fries. This immersive experience is perfect for food enthusiasts and those looking to enhance their culinary skills.", + "locationName": "Phuket Cooking Schools", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "phuket", + "ref": "thai-cooking-class-and-market-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_thai-cooking-class-and-market-tour.jpg" + }, + { + "name": "Relaxing Beach Day and Sunset Cruise", + "description": "Unwind and soak up the sun on the pristine beaches of Phuket. Choose from popular spots like Patong, Kata, or Karon Beach, and enjoy swimming, sunbathing, or simply relaxing by the turquoise waters. As the day ends, embark on a romantic sunset cruise along the coast, admiring the breathtaking views and enjoying a delicious dinner on board. This experience offers the perfect combination of relaxation and scenic beauty.", + "locationName": "Phuket Beaches", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "phuket", + "ref": "relaxing-beach-day-and-sunset-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_relaxing-beach-day-and-sunset-cruise.jpg" + }, + { + "name": "Phang Nga Bay Sea Kayaking", + "description": "Embark on a breathtaking sea kayaking adventure through the iconic Phang Nga Bay. Paddle through emerald-green waters, explore hidden caves and lagoons, and marvel at the towering limestone cliffs. Discover the famous James Bond Island and Koh Panyee, a floating village built on stilts.", + "locationName": "Phang Nga Bay", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "phuket", + "ref": "phang-nga-bay-sea-kayaking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_phang-nga-bay-sea-kayaking.jpg" + }, + { + "name": "Elephant Sanctuary Experience", + "description": "Connect with majestic elephants in an ethical and sustainable sanctuary. Learn about their behavior and conservation efforts, feed them, and observe them bathing in the mud. This unforgettable experience promotes responsible tourism and supports the well-being of these gentle giants.", + "locationName": "Elephant Sanctuary", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "phuket", + "ref": "elephant-sanctuary-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_elephant-sanctuary-experience.jpg" + }, + { + "name": "Bangla Road Nightlife", + "description": "Immerse yourself in the vibrant nightlife of Patong Beach on the infamous Bangla Road. Experience the bustling atmosphere, live music, street performances, and a variety of bars and clubs. This is the perfect place to let loose and enjoy the energetic nightlife of Phuket.", + "locationName": "Bangla Road, Patong Beach", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "phuket", + "ref": "bangla-road-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_bangla-road-nightlife.jpg" + }, + { + "name": "Karon Viewpoint Hike", + "description": "Embark on a scenic hike to Karon Viewpoint, offering panoramic views of Kata Noi Beach, Kata Beach, and Karon Beach. Capture stunning photos of the coastline and enjoy the fresh air and natural beauty of Phuket. This hike is suitable for various fitness levels and rewards you with breathtaking vistas.", + "locationName": "Karon Viewpoint", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "phuket", + "ref": "karon-viewpoint-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_karon-viewpoint-hike.jpg" + }, + { + "name": "Phuket Weekend Market", + "description": "Explore the vibrant Phuket Weekend Market, also known as Naka Market, and discover a treasure trove of local goods, souvenirs, clothing, and delicious street food. Immerse yourself in the bustling atmosphere, bargain with vendors, and experience the authentic local culture of Phuket.", + "locationName": "Phuket Weekend Market", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "phuket", + "ref": "phuket-weekend-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_phuket-weekend-market.jpg" + }, + { + "name": "Jungle Trekking and Waterfall Exploration", + "description": "Embark on an adventurous journey through Phuket's lush rainforests. Hike along scenic trails, discover hidden waterfalls like Bang Pae or Kathu Waterfall, take a refreshing dip in natural pools, and encounter diverse flora and fauna. This activity is perfect for nature lovers and those seeking an escape into the island's wild beauty.", + "locationName": "Khao Phra Thaeo National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "phuket", + "ref": "jungle-trekking-and-waterfall-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_jungle-trekking-and-waterfall-exploration.jpg" + }, + { + "name": "Simon Cabaret Show", + "description": "Experience the dazzling spectacle of the Simon Cabaret Show, one of Phuket's most famous nightlife attractions. Be entertained by talented performers in elaborate costumes, showcasing a vibrant blend of music, dance, and comedy. This is a unique cultural experience and a perfect way to enjoy an evening in Phuket.", + "locationName": "Patong Beach", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "phuket", + "ref": "simon-cabaret-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_simon-cabaret-show.jpg" + }, + { + "name": "Phuket Old Town Cultural Exploration", + "description": "Step back in time with a visit to Phuket Old Town. Wander through the charming streets lined with Sino-Portuguese architecture, explore historical landmarks like the Chinpracha House and the Thai Hua Museum, visit vibrant markets, and sample delicious local cuisine. This is a great way to immerse yourself in Phuket's rich history and culture.", + "locationName": "Phuket Old Town", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "phuket", + "ref": "phuket-old-town-cultural-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_phuket-old-town-cultural-exploration.jpg" + }, + { + "name": "Thai Massage and Spa Day", + "description": "Indulge in a relaxing and rejuvenating experience with a traditional Thai massage. Choose from a variety of treatments, including aromatherapy, herbal compresses, and foot reflexology, at one of Phuket's many luxurious spas. This is the perfect way to unwind and pamper yourself during your vacation.", + "locationName": "Various spas throughout Phuket", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "phuket", + "ref": "thai-massage-and-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_thai-massage-and-spa-day.jpg" + }, + { + "name": "Ziplining Adventure", + "description": "Soar through the rainforest canopy on an exhilarating ziplining adventure. Experience breathtaking views of the island as you zip between platforms, enjoying an adrenaline-pumping activity surrounded by nature. This is a perfect choice for thrill-seekers and adventure enthusiasts.", + "locationName": "Hanuman World or Flying Hanuman", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "phuket", + "ref": "ziplining-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_ziplining-adventure.jpg" + }, + { + "name": "Racha Yai and Coral Island Speedboat Day Trip", + "description": "Embark on a thrilling speedboat adventure to the pristine islands of Racha Yai and Coral Island. Dive into crystal-clear waters for snorkeling, discover vibrant coral reefs teeming with marine life, and bask on the soft sandy beaches. Enjoy a delicious lunch on board and soak up the breathtaking scenery. This action-packed day trip is perfect for adventure seekers and beach lovers.", + "locationName": "Racha Yai and Coral Island", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "phuket", + "ref": "racha-yai-and-coral-island-speedboat-day-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_racha-yai-and-coral-island-speedboat-day-trip.jpg" + }, + { + "name": "James Bond Island and Phang Nga Bay Tour by Longtail Boat", + "description": "Experience the iconic James Bond Island and the stunning Phang Nga Bay on a traditional longtail boat tour. Explore hidden lagoons, marvel at towering limestone cliffs, and visit the famous Koh Panyee floating village. Enjoy a delicious Thai lunch and capture unforgettable photos of this picturesque landscape.", + "locationName": "Phang Nga Bay", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "phuket", + "ref": "james-bond-island-and-phang-nga-bay-tour-by-longtail-boat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_james-bond-island-and-phang-nga-bay-tour-by-longtail-boat.jpg" + }, + { + "name": "FantaSea Show with Buffet Dinner", + "description": "Immerse yourself in the magic of Thai culture at the spectacular FantaSea show. Witness a captivating performance featuring acrobatics, illusions, and traditional dance, all set against a backdrop of stunning special effects. Indulge in a delicious buffet dinner with a variety of Thai and international dishes. This enchanting evening is perfect for families and culture enthusiasts.", + "locationName": "Kamala Beach", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "phuket", + "ref": "fantasea-show-with-buffet-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_fantasea-show-with-buffet-dinner.jpg" + }, + { + "name": "ATV Riding Adventure through the Jungle", + "description": "Get your adrenaline pumping with an exhilarating ATV ride through the lush jungles of Phuket. Navigate challenging terrains, discover hidden trails, and enjoy breathtaking views of the surrounding landscapes. This adventurous activity is perfect for thrill-seekers and nature lovers.", + "locationName": "Phuket's interior", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "phuket", + "ref": "atv-riding-adventure-through-the-jungle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_atv-riding-adventure-through-the-jungle.jpg" + }, + { + "name": "Romantic Sunset Dinner Cruise", + "description": "Indulge in a romantic evening aboard a luxurious yacht as you sail along the picturesque coastline of Phuket. Savor a delectable dinner with panoramic ocean views, sip on cocktails as the sun sets over the horizon, and create unforgettable memories with your loved one. This intimate experience is perfect for couples seeking a special getaway.", + "locationName": "Andaman Sea", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "phuket", + "ref": "romantic-sunset-dinner-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/phuket_romantic-sunset-dinner-cruise.jpg" + }, + { + "name": "Path of the Gods Hike", + "description": "Embark on a breathtaking hike along the Path of the Gods, a scenic trail offering panoramic views of the Amalfi Coast, the turquoise waters of the Mediterranean Sea, and charming villages nestled amidst terraced hillsides. This moderate hike is suitable for various fitness levels and rewards you with unforgettable vistas at every turn. Pack a picnic lunch to enjoy amidst the stunning scenery.", + "locationName": "Path of the Gods", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "positano", + "ref": "path-of-the-gods-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_path-of-the-gods-hike.jpg" + }, + { + "name": "Spiaggia Grande Beach Relaxation", + "description": "Soak up the sun and enjoy the vibrant atmosphere of Spiaggia Grande, Positano's main beach. Rent a sun lounger and umbrella, take a refreshing dip in the crystal-clear waters, or simply relax on the shore with a good book. Beachside cafes and restaurants offer delicious Italian fare and refreshing drinks, making it the perfect spot to unwind and people-watch.", + "locationName": "Spiaggia Grande", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "positano", + "ref": "spiaggia-grande-beach-relaxation", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_spiaggia-grande-beach-relaxation.jpg" + }, + { + "name": "Boat Trip to Capri", + "description": "Embark on a memorable boat trip to the captivating island of Capri. Explore the enchanting Blue Grotto, a sea cave illuminated with an ethereal blue light, wander through the charming towns of Capri and Anacapri, and admire the stunning coastal landscapes. This excursion offers a perfect blend of natural beauty, cultural exploration, and relaxation.", + "locationName": "Capri Island", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "positano", + "ref": "boat-trip-to-capri", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_boat-trip-to-capri.jpg" + }, + { + "name": "Positano Town Exploration", + "description": "Stroll through the charming streets of Positano, lined with colorful houses, boutique shops, and art galleries. Discover local crafts, indulge in delicious Italian gelato, and soak up the lively atmosphere. Visit the Church of Santa Maria Assunta, known for its majolica-tiled dome and Byzantine icon of the Virgin Mary, and explore the narrow alleyways that lead to hidden squares and breathtaking viewpoints.", + "locationName": "Positano Town", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "positano", + "ref": "positano-town-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_positano-town-exploration.jpg" + }, + { + "name": "Romantic Sunset Dinner", + "description": "Indulge in a romantic sunset dinner at one of Positano's cliffside restaurants. Savor delectable Italian cuisine, sip on fine wine, and enjoy breathtaking views of the coastline as the sun dips below the horizon. The enchanting ambiance and panoramic vistas create an unforgettable dining experience.", + "locationName": "Various cliffside restaurants", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "positano", + "ref": "romantic-sunset-dinner", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_romantic-sunset-dinner.jpg" + }, + { + "name": "Kayaking Along the Amalfi Coast", + "description": "Embark on a sea kayaking adventure, paddling along the crystal-clear waters of the Amalfi Coast. Explore hidden coves, sea caves, and secluded beaches inaccessible by foot. Enjoy breathtaking views of the coastline and the charming villages perched on the cliffs. This active excursion allows you to experience the beauty of the region from a unique perspective.", + "locationName": "Amalfi Coast", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "positano", + "ref": "kayaking-along-the-amalfi-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_kayaking-along-the-amalfi-coast.jpg" + }, + { + "name": "Cooking Class with a Local Chef", + "description": "Immerse yourself in Italian culinary culture with a hands-on cooking class. Learn the secrets of traditional dishes like fresh pasta, seafood specialties, and regional desserts from a local chef. Discover the art of selecting ingredients, mastering cooking techniques, and creating authentic Italian flavors. Enjoy the fruits of your labor with a delicious meal accompanied by local wine.", + "locationName": "Positano town", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "positano", + "ref": "cooking-class-with-a-local-chef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_cooking-class-with-a-local-chef.jpg" + }, + { + "name": "Lively Night at Music on the Rocks", + "description": "Experience Positano's vibrant nightlife at Music on the Rocks, a legendary nightclub carved into the cliffs. Dance the night away to the beats of renowned DJs and enjoy live music performances. Sip on cocktails and soak up the electric atmosphere with stunning coastal views as your backdrop.", + "locationName": "Music on the Rocks", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "positano", + "ref": "lively-night-at-music-on-the-rocks", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_lively-night-at-music-on-the-rocks.jpg" + }, + { + "name": "Scenic Drive along the Amalfi Coast", + "description": "Embark on a scenic drive along the winding roads of the Amalfi Coast. Rent a car or scooter and explore the picturesque villages, stopping at viewpoints to capture breathtaking panoramas. Visit charming towns like Ravello, Amalfi, and Atrani, each with its unique charm and historical sites. Enjoy the freedom to explore at your own pace and discover hidden gems along the way.", + "locationName": "Amalfi Coast", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "positano", + "ref": "scenic-drive-along-the-amalfi-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_scenic-drive-along-the-amalfi-coast.jpg" + }, + { + "name": "Shopping for Ceramics and Local Crafts", + "description": "Explore Positano's charming boutiques and workshops, discovering unique ceramics, handcrafted jewelry, and locally made souvenirs. Find beautifully painted ceramics, from decorative plates to intricate sculptures, showcasing the region's artistic traditions. Support local artisans and bring home a piece of Positano's charm.", + "locationName": "Positano town", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "positano", + "ref": "shopping-for-ceramics-and-local-crafts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_shopping-for-ceramics-and-local-crafts.jpg" + }, + { + "name": "Villa Cimbrone Gardens Exploration", + "description": "Step into a world of enchantment at the Villa Cimbrone Gardens, a historic villa boasting breathtaking panoramic views of the Amalfi Coast. Wander through lush gardens adorned with sculptures, fountains, and hidden pathways, leading to the Terrace of Infinity, offering unparalleled vistas that will leave you speechless. Immerse yourself in the beauty and tranquility of this botanical paradise.", + "locationName": "Villa Cimbrone", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "positano", + "ref": "villa-cimbrone-gardens-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_villa-cimbrone-gardens-exploration.jpg" + }, + { + "name": "Scuba Diving Adventure in the Mediterranean", + "description": "Dive into the crystal-clear waters of the Mediterranean Sea and discover a vibrant underwater world teeming with marine life. Explore hidden coves, underwater caves, and ancient shipwrecks, encountering colorful fish, playful dolphins, and graceful sea turtles. Whether you're a seasoned diver or a beginner, Positano offers unforgettable scuba diving experiences for all levels.", + "locationName": "Various diving centers along the Amalfi Coast", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "positano", + "ref": "scuba-diving-adventure-in-the-mediterranean", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_scuba-diving-adventure-in-the-mediterranean.jpg" + }, + { + "name": "Wine Tasting Tour in the Hills of Furore", + "description": "Embark on a delightful journey through the vineyards of Furore, a hidden gem nestled in the hills above Positano. Visit local wineries, sample exquisite regional wines, and learn about the traditional winemaking techniques passed down through generations. Indulge in the flavors of the Amalfi Coast while enjoying breathtaking views of the surrounding countryside.", + "locationName": "Furore wineries", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "positano", + "ref": "wine-tasting-tour-in-the-hills-of-furore", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_wine-tasting-tour-in-the-hills-of-furore.jpg" + }, + { + "name": "Catch a Performance at Teatro La Fenice", + "description": "Immerse yourself in the vibrant cultural scene of Positano with a captivating performance at Teatro La Fenice. This intimate theater hosts a variety of events, including concerts, plays, and dance shows, showcasing local and international talent. Experience the magic of live entertainment in a charming setting.", + "locationName": "Teatro La Fenice", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "positano", + "ref": "catch-a-performance-at-teatro-la-fenice", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_catch-a-performance-at-teatro-la-fenice.jpg" + }, + { + "name": "Indulge in a Spa Day with a View", + "description": "Escape the hustle and bustle and treat yourself to a luxurious spa day with breathtaking views of the Amalfi Coast. Relax and rejuvenate with a variety of treatments, including massages, facials, and body wraps, while enjoying the serene ambiance and stunning scenery. Several hotels and wellness centers in Positano offer spa packages with panoramic views.", + "locationName": "Various hotels and wellness centers", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "positano", + "ref": "indulge-in-a-spa-day-with-a-view", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_indulge-in-a-spa-day-with-a-view.jpg" + }, + { + "name": "Lemon Grove Tour and Limoncello Tasting", + "description": "Explore the fragrant lemon groves that Positano is famous for. Learn about the cultivation of these citrus fruits and their significance to the local economy. Enjoy a tasting of limoncello, the region's beloved lemon liqueur, and other lemon-infused treats. This sensory experience offers a unique glimpse into the agricultural heritage of the Amalfi Coast.", + "locationName": "Local lemon grove", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "positano", + "ref": "lemon-grove-tour-and-limoncello-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_lemon-grove-tour-and-limoncello-tasting.jpg" + }, + { + "name": "Stand Up Paddleboarding Excursion", + "description": "Embark on a stand-up paddleboarding adventure along the Positano coastline. Enjoy the tranquility of the sea while taking in breathtaking views of the cliffs and colorful houses. This activity is suitable for various skill levels and offers a fun and active way to explore the area from a different perspective.", + "locationName": "Positano coastline", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "positano", + "ref": "stand-up-paddleboarding-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_stand-up-paddleboarding-excursion.jpg" + }, + { + "name": "Grotta dello Smeraldo Cave Exploration", + "description": "Discover the enchanting Grotta dello Smeraldo, a sea cave known for its emerald-green waters. Take a boat tour to the cave and marvel at the unique light reflections that create a magical atmosphere. This natural wonder is a must-see for visitors to Positano.", + "locationName": "Grotta dello Smeraldo", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "positano", + "ref": "grotta-dello-smeraldo-cave-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_grotta-dello-smeraldo-cave-exploration.jpg" + }, + { + "name": "Li Galli Island Boat Trip and Snorkeling", + "description": "Escape to the secluded Li Galli archipelago, a group of small islands off the coast of Positano. Enjoy a boat trip to these pristine islands and discover their hidden coves and beaches. Go snorkeling in the crystal-clear waters and admire the vibrant marine life. This excursion offers a perfect blend of relaxation and adventure.", + "locationName": "Li Galli Islands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "positano", + "ref": "li-galli-island-boat-trip-and-snorkeling", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_li-galli-island-boat-trip-and-snorkeling.jpg" + }, + { + "name": "Live Music and Dancing at Franco's Bar", + "description": "Experience the vibrant nightlife of Positano at Franco's Bar, a legendary venue known for its live music and lively atmosphere. Enjoy cocktails, dance the night away, and soak up the energetic ambiance. This is a perfect option for those looking for a fun and memorable night out.", + "locationName": "Franco's Bar", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "positano", + "ref": "live-music-and-dancing-at-franco-s-bar", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/positano_live-music-and-dancing-at-franco-s-bar.jpg" + }, + { + "name": "Explore Prague Castle", + "description": "Embark on a journey through time at the magnificent Prague Castle, the largest ancient castle complex in the world. Discover St. Vitus Cathedral, witness the Changing of the Guard ceremony, and explore the Golden Lane with its charming houses and workshops.", + "locationName": "Prague Castle", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "prague", + "ref": "explore-prague-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_explore-prague-castle.jpg" + }, + { + "name": "Wander through Old Town Square", + "description": "Immerse yourself in the heart of Prague at the Old Town Square. Admire the Astronomical Clock, marvel at the Gothic architecture of the Týn Church, and enjoy the vibrant atmosphere of street performers and local vendors.", + "locationName": "Old Town Square", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "wander-through-old-town-square", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_wander-through-old-town-square.jpg" + }, + { + "name": "Cruise on the Vltava River", + "description": "Enjoy a relaxing boat tour along the Vltava River, offering stunning views of the city's iconic landmarks. Admire the Charles Bridge, the National Theatre, and the Prague Castle from a unique perspective while enjoying the gentle breeze and the city's skyline.", + "locationName": "Vltava River", + "duration": 1, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "prague", + "ref": "cruise-on-the-vltava-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_cruise-on-the-vltava-river.jpg" + }, + { + "name": "Discover the Jewish Quarter", + "description": "Delve into the rich history and culture of Prague's Jewish Quarter. Explore the synagogues, including the Old-New Synagogue, the oldest active synagogue in Europe, and visit the Old Jewish Cemetery, a poignant reminder of the community's past.", + "locationName": "Josefov", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "discover-the-jewish-quarter", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_discover-the-jewish-quarter.jpg" + }, + { + "name": "Savor Traditional Czech Cuisine", + "description": "Indulge in the hearty flavors of traditional Czech cuisine. Sample classic dishes like vepřo knedlo zelo (roast pork with dumplings and cabbage), guláš (beef stew), and smažený sýr (fried cheese), accompanied by a refreshing Pilsner Urquell beer.", + "locationName": "Various restaurants", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "savor-traditional-czech-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_savor-traditional-czech-cuisine.jpg" + }, + { + "name": "Petrin Hill Viewpoint", + "description": "Take a funicular ride or hike up Petrin Hill for breathtaking panoramic views of Prague. Explore Petrin Gardens, visit the Petrin Lookout Tower (a mini Eiffel Tower!), or wander through the rose gardens. This activity offers stunning views and a peaceful escape from the city.", + "locationName": "Petrin Hill", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "petrin-hill-viewpoint", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_petrin-hill-viewpoint.jpg" + }, + { + "name": "Vyšehrad Fortress", + "description": "Discover the historical Vyšehrad Fortress, a complex with stunning views, beautiful gardens, and significant landmarks like the Basilica of St. Peter and St. Paul and the Vyšehrad Cemetery. Explore the ancient walls, delve into Czech legends, and enjoy a picnic with scenic views.", + "locationName": "Vyšehrad", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "vy-ehrad-fortress", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_vy-ehrad-fortress.jpg" + }, + { + "name": "Beer Culture Experience", + "description": "Immerse yourself in Prague's renowned beer culture. Take a brewery tour to learn about the brewing process, visit traditional beer halls to sample local brews, or join a beer tasting session to discover unique flavors. This activity is perfect for beer enthusiasts and those wanting to experience Czech culture.", + "locationName": "Various breweries and beer halls", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "prague", + "ref": "beer-culture-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_beer-culture-experience.jpg" + }, + { + "name": "John Lennon Wall", + "description": "Visit the iconic John Lennon Wall, a symbol of peace and freedom. Admire the ever-changing graffiti art, leave your own message, and soak in the inspiring atmosphere. This activity is perfect for art lovers, music fans, and those seeking a unique cultural experience.", + "locationName": "John Lennon Wall", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "prague", + "ref": "john-lennon-wall", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_john-lennon-wall.jpg" + }, + { + "name": "Black Light Theater", + "description": "Experience the magic of Black Light Theater, a unique theatrical performance that combines UV lights, fluorescent costumes, and illusions. Enjoy a visually stunning and captivating show that tells a story through movement and music.", + "locationName": "Various theaters in Prague", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "prague", + "ref": "black-light-theater", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_black-light-theater.jpg" + }, + { + "name": "Delve into the World of Franz Kafka", + "description": "Embark on a literary journey exploring the life and works of Prague's most famous author, Franz Kafka. Visit the Franz Kafka Museum, which delves into his complex life and literary contributions, and wander through the streets that inspired his surrealist writings. For a deeper dive, consider joining a guided Kafka-themed walking tour to uncover hidden gems and gain unique insights into his influence on the city.", + "locationName": "Franz Kafka Museum and various locations throughout Prague", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "prague", + "ref": "delve-into-the-world-of-franz-kafka", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_delve-into-the-world-of-franz-kafka.jpg" + }, + { + "name": "Experience the Magic of the National Theatre", + "description": "Immerse yourself in the cultural heart of Prague with a visit to the National Theatre. Catch a captivating opera, ballet, or drama performance in this architectural masterpiece. Even if you're not attending a show, take a guided tour to admire the opulent interiors and learn about the theatre's rich history.", + "locationName": "National Theatre", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "prague", + "ref": "experience-the-magic-of-the-national-theatre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_experience-the-magic-of-the-national-theatre.jpg" + }, + { + "name": "Shop for Treasures at Havelské Market", + "description": "Indulge in a sensory experience at Havelské Market, the oldest outdoor market in Prague. Browse through stalls brimming with fresh produce, local handicrafts, souvenirs, and unique Czech specialties. Sample traditional pastries, cheeses, and cured meats while soaking in the vibrant atmosphere.", + "locationName": "Havelské Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "shop-for-treasures-at-havelsk-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_shop-for-treasures-at-havelsk-market.jpg" + }, + { + "name": "Escape to the Tranquility of Vrtba Garden", + "description": "Find a peaceful oasis amidst the bustling city at Vrtba Garden, a hidden gem of Baroque landscape architecture. Wander through terraced gardens adorned with sculptures, fountains, and vibrant flowerbeds. Enjoy panoramic views of the city from the upper terraces and escape the urban rush in this serene setting.", + "locationName": "Vrtba Garden", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "escape-to-the-tranquility-of-vrtba-garden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_escape-to-the-tranquility-of-vrtba-garden.jpg" + }, + { + "name": "Take a Day Trip to the Enchanting Karlštejn Castle", + "description": "Embark on a scenic day trip to Karlštejn Castle, a medieval fortress nestled amidst rolling hills just outside of Prague. Explore the castle's grand halls, towers, and courtyards, and learn about its fascinating history as a repository for royal treasures. Enjoy breathtaking views of the surrounding countryside and experience a journey back in time.", + "locationName": "Karlštejn", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "prague", + "ref": "take-a-day-trip-to-the-enchanting-karl-tejn-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_take-a-day-trip-to-the-enchanting-karl-tejn-castle.jpg" + }, + { + "name": "Day Trip to Kutná Hora", + "description": "Embark on a captivating day trip to the UNESCO-listed town of Kutná Hora, renowned for its Sedlec Ossuary, a unique chapel adorned with human bones. Explore the historic center, visit the magnificent St. Barbara's Church, and descend into the eerie depths of the silver mines.", + "locationName": "Kutná Hora", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "prague", + "ref": "day-trip-to-kutn-hora", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_day-trip-to-kutn-hora.jpg" + }, + { + "name": "Paddleboarding on the Vltava River", + "description": "Experience Prague from a different perspective with a stand-up paddleboarding adventure on the Vltava River. Glide beneath the city's iconic bridges, admire the picturesque skyline, and enjoy a unique and active way to explore the city.", + "locationName": "Vltava River", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "paddleboarding-on-the-vltava-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_paddleboarding-on-the-vltava-river.jpg" + }, + { + "name": "Unwind at a Traditional Beer Spa", + "description": "Indulge in a unique and relaxing experience at a traditional Czech beer spa. Immerse yourself in a tub filled with natural ingredients like hops and barley, while enjoying unlimited beer on tap. This rejuvenating treatment is a perfect way to unwind after a day of exploring.", + "locationName": "Various Beer Spas throughout Prague", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "prague", + "ref": "unwind-at-a-traditional-beer-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_unwind-at-a-traditional-beer-spa.jpg" + }, + { + "name": "Catch a Classical Concert", + "description": "Immerse yourself in Prague's rich musical heritage by attending a classical concert. Choose from a variety of venues, including historic churches, concert halls, and even intimate settings, to experience the enchanting melodies of renowned composers.", + "locationName": "Various concert halls and churches throughout Prague", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "catch-a-classical-concert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_catch-a-classical-concert.jpg" + }, + { + "name": "Explore the Trendy Vinohrady District", + "description": "Venture beyond the historic center and discover the trendy Vinohrady district. Explore its charming streets lined with Art Nouveau buildings, relax in one of the many cafes and restaurants, or browse through independent shops and boutiques.", + "locationName": "Vinohrady District", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "prague", + "ref": "explore-the-trendy-vinohrady-district", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/prague_explore-the-trendy-vinohrady-district.jpg" + }, + { + "name": "Explore Old San Juan", + "description": "Wander through the cobblestone streets of Old San Juan, a UNESCO World Heritage Site, and admire the colorful Spanish colonial architecture. Visit historic forts like Castillo San Felipe del Morro and Castillo San Cristobal, offering stunning ocean views. Explore local shops, art galleries, and museums, and savor delicious Puerto Rican cuisine at charming cafes and restaurants.", + "locationName": "Old San Juan", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "puerto-rico", + "ref": "explore-old-san-juan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_explore-old-san-juan.jpg" + }, + { + "name": "Relax on Flamenco Beach", + "description": "Escape to the pristine shores of Flamenco Beach, consistently ranked among the world's best beaches. Sink your toes into the soft white sand, swim in the crystal-clear turquoise waters, and soak up the Caribbean sun. Snorkel or scuba dive to discover vibrant coral reefs teeming with marine life. Enjoy beachside amenities, water sports rentals, and delicious food kiosks.", + "locationName": "Flamenco Beach, Culebra Island", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "puerto-rico", + "ref": "relax-on-flamenco-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_relax-on-flamenco-beach.jpg" + }, + { + "name": "Hike in El Yunque National Forest", + "description": "Embark on a hike through the lush rainforests of El Yunque National Forest, the only tropical rainforest in the US National Forest System. Discover cascading waterfalls, unique flora and fauna, and breathtaking panoramic views. Hike to La Mina Falls for a refreshing dip, or climb the Yokahu Tower for stunning vistas. Choose from various trails suitable for different fitness levels.", + "locationName": "El Yunque National Forest", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "puerto-rico", + "ref": "hike-in-el-yunque-national-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_hike-in-el-yunque-national-forest.jpg" + }, + { + "name": "Kayak in a Bioluminescent Bay", + "description": "Experience the magic of a bioluminescent bay, where the water glows with tiny organisms called dinoflagellates. Paddle through the darkness in a kayak and witness the mesmerizing blue-green light illuminate with every movement. Choose from three bioluminescent bays in Puerto Rico: Mosquito Bay (Vieques), Laguna Grande (Fajardo), or La Parguera (Lajas).", + "locationName": "Bioluminescent Bays (Vieques, Fajardo, or Lajas)", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "puerto-rico", + "ref": "kayak-in-a-bioluminescent-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_kayak-in-a-bioluminescent-bay.jpg" + }, + { + "name": "Go Salsa Dancing", + "description": "Immerse yourself in the vibrant nightlife of San Juan and experience the rhythm of salsa dancing. Take a salsa lesson or join a dance social at a local club or bar. Enjoy live music, delicious cocktails, and the infectious energy of the dance floor. Learn basic steps or show off your advanced moves, and let loose to the pulsating beats of salsa.", + "locationName": "San Juan", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "puerto-rico", + "ref": "go-salsa-dancing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_go-salsa-dancing.jpg" + }, + { + "name": "Caving Adventure in Río Camuy Cave Park", + "description": "Embark on a thrilling journey into the depths of the third-largest cave system in the world. Explore the majestic caverns adorned with stalactites and stalagmites, marvel at the underground river, and learn about the unique ecosystem within.", + "locationName": "Río Camuy Cave Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "puerto-rico", + "ref": "caving-adventure-in-r-o-camuy-cave-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_caving-adventure-in-r-o-camuy-cave-park.jpg" + }, + { + "name": "Horseback Riding through the Countryside", + "description": "Experience the beauty of Puerto Rico's lush landscapes on horseback. Ride through scenic trails, passing by plantations, rolling hills, and breathtaking coastal views. This is a perfect activity for nature lovers and those seeking a relaxing yet adventurous experience.", + "locationName": "Hacienda Carabalí", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "puerto-rico", + "ref": "horseback-riding-through-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_horseback-riding-through-the-countryside.jpg" + }, + { + "name": "Ziplining through the Rainforest Canopy", + "description": "Soar through the air on a thrilling zipline adventure, experiencing the rainforest from a unique perspective. Enjoy breathtaking views of the lush foliage, cascading waterfalls, and the distant ocean as you zip between platforms.", + "locationName": "Toro Verde Nature Adventure Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "puerto-rico", + "ref": "ziplining-through-the-rainforest-canopy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_ziplining-through-the-rainforest-canopy.jpg" + }, + { + "name": "Taste the Flavors of Puerto Rico on a Food Tour", + "description": "Embark on a culinary journey through the vibrant streets of San Juan, indulging in the diverse flavors of Puerto Rican cuisine. Sample local delicacies, learn about traditional cooking methods, and discover hidden culinary gems.", + "locationName": "Old San Juan", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "puerto-rico", + "ref": "taste-the-flavors-of-puerto-rico-on-a-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_taste-the-flavors-of-puerto-rico-on-a-food-tour.jpg" + }, + { + "name": "Sunset Sail along the Coast", + "description": "Set sail on a romantic catamaran cruise as the sun begins its descent, painting the sky with vibrant hues. Enjoy breathtaking views of the coastline, sip on refreshing cocktails, and create unforgettable memories.", + "locationName": "Fajardo", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "puerto-rico", + "ref": "sunset-sail-along-the-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_sunset-sail-along-the-coast.jpg" + }, + { + "name": "Snorkeling Adventure at Cayo Icacos", + "description": "Embark on a boat trip to the uninhabited island of Cayo Icacos, a true gem off the coast of Fajardo. Dive into crystal-clear waters and discover a vibrant underwater world teeming with colorful fish and coral reefs. This snorkeling adventure offers a glimpse into the island's diverse marine life and is perfect for all skill levels.", + "locationName": "Cayo Icacos", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "puerto-rico", + "ref": "snorkeling-adventure-at-cayo-icacos", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_snorkeling-adventure-at-cayo-icacos.jpg" + }, + { + "name": "Coffee Plantation Tour and Tasting", + "description": "Delve into the world of Puerto Rican coffee with a visit to a local coffee plantation. Explore the lush fields, learn about the coffee-making process from bean to cup, and indulge in a tasting of freshly brewed coffee. This cultural experience offers insight into the island's rich coffee heritage and is a must for coffee enthusiasts.", + "locationName": "Hacienda Buena Vista or Hacienda Lealtad", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "puerto-rico", + "ref": "coffee-plantation-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_coffee-plantation-tour-and-tasting.jpg" + }, + { + "name": "Stand-Up Paddleboarding in Condado Lagoon", + "description": "Experience the tranquility of Condado Lagoon on a stand-up paddleboard. Glide across the calm waters, surrounded by the cityscape and lush greenery. This activity is perfect for a relaxing afternoon, offering a unique perspective of the San Juan area and a chance to connect with nature.", + "locationName": "Condado Lagoon", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "puerto-rico", + "ref": "stand-up-paddleboarding-in-condado-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_stand-up-paddleboarding-in-condado-lagoon.jpg" + }, + { + "name": "Stargazing at Arecibo Observatory", + "description": "Embark on a celestial journey at the Arecibo Observatory, home to one of the world's largest radio telescopes. Learn about the cosmos, participate in a stargazing session, and marvel at the wonders of the night sky. This unique experience offers a glimpse into the world of astronomy and is perfect for those seeking a nighttime adventure.", + "locationName": "Arecibo Observatory", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "puerto-rico", + "ref": "stargazing-at-arecibo-observatory", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_stargazing-at-arecibo-observatory.jpg" + }, + { + "name": "Castillo San Cristóbal Exploration", + "description": "Step back in time with a visit to Castillo San Cristóbal, a 17th-century Spanish fort offering breathtaking views of San Juan. Explore the historic ramparts, tunnels, and dungeons, and learn about the fort's role in protecting the island from invaders. This journey into the past is perfect for history buffs and those seeking panoramic views.", + "locationName": "Castillo San Cristóbal", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "puerto-rico", + "ref": "castillo-san-crist-bal-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_castillo-san-crist-bal-exploration.jpg" + }, + { + "name": "Learn to Surf at Rincon", + "description": "Catch some waves and experience the thrill of surfing in Rincon, known as the 'Surfing Capital of the Caribbean.' Whether you're a beginner or an experienced surfer, Rincon offers waves for all skill levels. Take a lesson from a local surf school and enjoy the beautiful beaches while learning a new skill.", + "locationName": "Rincon", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "puerto-rico", + "ref": "learn-to-surf-at-rincon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_learn-to-surf-at-rincon.jpg" + }, + { + "name": "Explore the Colorful Streets of Ponce", + "description": "Wander through the historic city of Ponce, known for its colorful colonial architecture, charming plazas, and vibrant art scene. Visit the Ponce Museum of Art, home to an extensive collection of European and Puerto Rican art, or explore the historic Serrallés Castle for a glimpse into the island's sugar cane industry past.", + "locationName": "Ponce", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "puerto-rico", + "ref": "explore-the-colorful-streets-of-ponce", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_explore-the-colorful-streets-of-ponce.jpg" + }, + { + "name": "Dive into History at Castillo San Felipe del Morro", + "description": "Step back in time and explore the impressive Castillo San Felipe del Morro, a 16th-century citadel that guarded the entrance to San Juan Bay. Discover the fort's rich history, walk through its tunnels and ramparts, and enjoy breathtaking views of the ocean.", + "locationName": "San Juan", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "puerto-rico", + "ref": "dive-into-history-at-castillo-san-felipe-del-morro", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_dive-into-history-at-castillo-san-felipe-del-morro.jpg" + }, + { + "name": "Go on a Culinary Adventure", + "description": "Embark on a culinary journey through Puerto Rico's diverse food scene. Sample traditional dishes like mofongo, arroz con gandules, and pasteles, or indulge in fresh seafood and tropical fruits. Explore local markets, take a cooking class, or dine at renowned restaurants to experience the island's unique flavors.", + "locationName": "Various Locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "puerto-rico", + "ref": "go-on-a-culinary-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_go-on-a-culinary-adventure.jpg" + }, + { + "name": "Discover the Enchanting Guánica Dry Forest", + "description": "Explore the unique ecosystem of the Guánica Dry Forest, a UNESCO Biosphere Reserve. Hike through the dry, subtropical forest, home to diverse plant and animal life, including rare bird species and cacti. Enjoy stunning views of the Caribbean Sea and learn about the importance of conservation efforts.", + "locationName": "Guánica", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "puerto-rico", + "ref": "discover-the-enchanting-gu-nica-dry-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/puerto-rico_discover-the-enchanting-gu-nica-dry-forest.jpg" + }, + { + "name": "Bavaro Beach Bliss", + "description": "Indulge in the quintessential Punta Cana experience with a day of relaxation on the stunning Bavaro Beach. Bask in the sun on the powdery white sand, take a refreshing dip in the turquoise waters, or simply unwind under the shade of a swaying palm tree. The beach offers a variety of water sports options, including snorkeling, kayaking, and paddleboarding, for those seeking a bit more adventure.", + "locationName": "Bavaro Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "punta-cana", + "ref": "bavaro-beach-bliss", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_bavaro-beach-bliss.jpg" + }, + { + "name": "Underwater Adventures", + "description": "Explore the vibrant underwater world of Punta Cana with a snorkeling or scuba diving excursion. Discover colorful coral reefs teeming with tropical fish, graceful sea turtles, and other fascinating marine life. Numerous dive centers offer guided tours and courses for all levels, making it an unforgettable experience for both beginners and experienced divers.", + "locationName": "Various dive sites along the coast", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "punta-cana", + "ref": "underwater-adventures", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_underwater-adventures.jpg" + }, + { + "name": "Hoyo Azul Eco Tour", + "description": "Embark on an eco-adventure to Hoyo Azul, a hidden natural wonder tucked away in the Scape Park. Hike through lush rainforest trails, descend into a cenote with crystal-clear turquoise waters, and take a refreshing swim in this magical oasis. The tour also includes a visit to the Indigenous Eyes Ecological Park, where you can explore a series of lagoons and learn about the local flora and fauna.", + "locationName": "Scape Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "punta-cana", + "ref": "hoyo-azul-eco-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_hoyo-azul-eco-tour.jpg" + }, + { + "name": "Flavors of the Dominican Republic", + "description": "Embark on a culinary journey and discover the rich flavors of Dominican cuisine. Take a cooking class and learn to prepare traditional dishes such as La Bandera (rice, beans, and meat), Pescado con Coco (fish with coconut sauce), or Sancocho (a hearty stew). Alternatively, join a food tour and sample local delicacies at various restaurants and street food stalls.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "punta-cana", + "ref": "flavors-of-the-dominican-republic", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_flavors-of-the-dominican-republic.jpg" + }, + { + "name": "Indigenous Eyes Ecological Park", + "description": "Immerse yourself in nature at the Indigenous Eyes Ecological Park, a sanctuary of lagoons, lush vegetation, and diverse wildlife. Take a leisurely walk along the trails, swim in the refreshing lagoons, and learn about the park's conservation efforts. The park is also home to a petting zoo and a cultural village, providing a glimpse into the Dominican way of life.", + "locationName": "Indigenous Eyes Ecological Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "punta-cana", + "ref": "indigenous-eyes-ecological-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_indigenous-eyes-ecological-park.jpg" + }, + { + "name": "Saona Island Escape", + "description": "Embark on a full-day excursion to the breathtaking Saona Island, a tropical paradise boasting pristine beaches, swaying palm trees, and turquoise waters. Cruise along the coastline, snorkel among vibrant coral reefs, and indulge in a delicious Dominican feast on the beach. Relax in a hammock, soak up the sun, and experience the ultimate island getaway.", + "locationName": "Saona Island", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "punta-cana", + "ref": "saona-island-escape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_saona-island-escape.jpg" + }, + { + "name": "Horseback Riding Adventure", + "description": "Explore the Dominican countryside on horseback, traversing lush trails, scenic landscapes, and local villages. Experience the beauty of nature, interact with friendly locals, and create unforgettable memories. This activity is suitable for all skill levels and offers a unique perspective of Punta Cana's natural wonders.", + "locationName": "Dominican countryside", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "punta-cana", + "ref": "horseback-riding-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_horseback-riding-adventure.jpg" + }, + { + "name": "ChocoMuseo Chocolate Experience", + "description": "Immerse yourself in the world of Dominican chocolate at the ChocoMuseo. Learn about the history of cocoa, participate in a chocolate-making workshop, and indulge in delicious tastings. Discover the bean-to-bar process, create your own chocolate treats, and explore the museum's fascinating exhibits.", + "locationName": "ChocoMuseo", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "punta-cana", + "ref": "chocomuseo-chocolate-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_chocomuseo-chocolate-experience.jpg" + }, + { + "name": "Marinarium Snorkeling Cruise", + "description": "Embark on a catamaran cruise to a vibrant marine sanctuary teeming with marine life. Snorkel among colorful fish, graceful stingrays, and playful nurse sharks. Enjoy refreshing drinks, lively music, and stunning views of the Dominican coastline. This adventure offers a perfect blend of relaxation and underwater exploration.", + "locationName": "Marinarium", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "punta-cana", + "ref": "marinarium-snorkeling-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_marinarium-snorkeling-cruise.jpg" + }, + { + "name": "Coco Bongo Nightlife Extravaganza", + "description": "Experience the electrifying nightlife of Punta Cana at Coco Bongo, a world-renowned nightclub. Immerse yourself in a dazzling show featuring acrobats, dancers, live music, and impressive special effects. Dance the night away, enjoy unlimited drinks, and create unforgettable memories in this vibrant and energetic atmosphere.", + "locationName": "Coco Bongo", + "duration": 5, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "punta-cana", + "ref": "coco-bongo-nightlife-extravaganza", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_coco-bongo-nightlife-extravaganza.jpg" + }, + { + "name": "Scape Park Adventure", + "description": "Embark on a thrilling journey through Scape Park, a natural theme park boasting captivating cenotes, exhilarating zip lines, and exciting cave expeditions. Discover hidden waterfalls, swim in crystal-clear waters, and challenge yourself with adventurous activities amidst the Dominican jungle.", + "locationName": "Scape Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "punta-cana", + "ref": "scape-park-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_scape-park-adventure.jpg" + }, + { + "name": "Sunset Catamaran Cruise", + "description": "Set sail on a romantic catamaran cruise as the sun dips below the horizon, painting the sky with vibrant hues. Enjoy breathtaking ocean views, sip on tropical cocktails, and dance to the rhythm of local music as you create unforgettable memories.", + "locationName": "Punta Cana coastline", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "punta-cana", + "ref": "sunset-catamaran-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_sunset-catamaran-cruise.jpg" + }, + { + "name": "Indigenous Cooking Class", + "description": "Immerse yourself in Dominican culture with a hands-on cooking class, learning to prepare traditional dishes using fresh, local ingredients. Discover the secrets of flavorful Dominican cuisine and savor the delicious results of your culinary adventure.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "punta-cana", + "ref": "indigenous-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_indigenous-cooking-class.jpg" + }, + { + "name": "Ocean Spa Experience", + "description": "Indulge in a rejuvenating spa experience with oceanfront views, soothing massages, and revitalizing body treatments. Unwind and reconnect with yourself amidst the tranquil sounds of the waves and the gentle ocean breeze.", + "locationName": "Various resorts and spas", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "punta-cana", + "ref": "ocean-spa-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_ocean-spa-experience.jpg" + }, + { + "name": "Explore Altos de Chavón", + "description": "Step back in time at Altos de Chavón, a replica of a 16th-century Mediterranean village. Wander through cobblestone streets, admire the charming architecture, and discover local art galleries and artisan shops. Enjoy stunning views of the Chavón River and immerse yourself in the cultural heritage of the Dominican Republic.", + "locationName": "Altos de Chavón", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "punta-cana", + "ref": "explore-altos-de-chav-n", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_explore-altos-de-chav-n.jpg" + }, + { + "name": "Zipline Adventure Through the Jungle", + "description": "Soar through the lush Dominican rainforest canopy on an exhilarating zipline adventure. Experience breathtaking views of the tropical landscape as you glide from platform to platform, feeling the adrenaline rush and enjoying the cool breeze. This activity is perfect for thrill-seekers and nature lovers alike, offering a unique perspective of the Dominican wilderness.", + "locationName": "Anamuya Mountains", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "punta-cana", + "ref": "zipline-adventure-through-the-jungle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_zipline-adventure-through-the-jungle.jpg" + }, + { + "name": "Sunset Horseback Riding on the Beach", + "description": "Embark on a romantic and unforgettable horseback riding experience along the pristine shores of Punta Cana. As the sun begins its descent, casting a warm glow over the turquoise waters, you'll ride gentle horses through the soft sand, enjoying the tranquil beauty of the beach and the sound of waves crashing against the shore. This is a perfect way to connect with nature and create lasting memories.", + "locationName": "Uvero Alto Beach", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "punta-cana", + "ref": "sunset-horseback-riding-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_sunset-horseback-riding-on-the-beach.jpg" + }, + { + "name": "Dominican Dance Class", + "description": "Immerse yourself in the vibrant Dominican culture with a lively dance class. Learn the steps to traditional dances like Merengue and Bachata, guided by experienced instructors who will share their passion and energy. This is a fun and interactive way to connect with local culture, meet new people, and add some rhythm to your vacation.", + "locationName": "Local dance studio or resort", + "duration": 1.5, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "punta-cana", + "ref": "dominican-dance-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_dominican-dance-class.jpg" + }, + { + "name": "Catamaran Sailing and Snorkeling Trip", + "description": "Set sail on a luxurious catamaran and embark on a journey along the stunning coastline of Punta Cana. Enjoy the warm Caribbean breeze as you cruise through crystal-clear waters, stopping at vibrant coral reefs for snorkeling adventures. Discover the colorful underwater world teeming with marine life, and later, indulge in a delicious lunch on board while soaking up the sun and enjoying the breathtaking views.", + "locationName": "Punta Cana coastline", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "punta-cana", + "ref": "catamaran-sailing-and-snorkeling-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_catamaran-sailing-and-snorkeling-trip.jpg" + }, + { + "name": "Rum Distillery Tour and Tasting", + "description": "Delve into the history and production of Dominican rum with a fascinating tour of a local distillery. Learn about the sugarcane cultivation process, the distillation techniques, and the aging methods that create the unique flavors of Dominican rum. Afterward, indulge in a tasting session, sampling various rum varieties and discovering your favorite.", + "locationName": "Ron Barceló Distillery", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "punta-cana", + "ref": "rum-distillery-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/punta-cana_rum-distillery-tour-and-tasting.jpg" + }, + { + "name": "Explore the Historic Old Town", + "description": "Step back in time and wander through the cobblestone streets of Old Quebec, a UNESCO World Heritage Site. Admire the charming architecture, visit historic landmarks like the Notre-Dame de Québec Basilica-Cathedral, and soak up the European ambiance.", + "locationName": "Old Quebec", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "quebec-city", + "ref": "explore-the-historic-old-town", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_explore-the-historic-old-town.jpg" + }, + { + "name": "Visit the Iconic Chateau Frontenac", + "description": "Discover the grandeur of the Fairmont Le Château Frontenac, a historic luxury hotel and one of Quebec City's most recognizable landmarks. Take a guided tour, enjoy afternoon tea, or simply admire its architectural beauty from Dufferin Terrace.", + "locationName": "Chateau Frontenac", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "quebec-city", + "ref": "visit-the-iconic-chateau-frontenac", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_visit-the-iconic-chateau-frontenac.jpg" + }, + { + "name": "Cruise the St. Lawrence River", + "description": "Embark on a scenic boat tour along the St. Lawrence River and enjoy breathtaking views of the city skyline, the Laurentian Mountains, and the Île d'Orléans. Choose from various options, including sightseeing cruises, dinner cruises, or even whale watching excursions.", + "locationName": "St. Lawrence River", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "quebec-city", + "ref": "cruise-the-st-lawrence-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_cruise-the-st-lawrence-river.jpg" + }, + { + "name": "Indulge in French Canadian Cuisine", + "description": "Delight your taste buds with the unique flavors of French Canadian cuisine. Sample traditional dishes like poutine, tourtière, and maple syrup pie at local restaurants or bistros. Don't miss the chance to explore the city's vibrant culinary scene.", + "locationName": "Various restaurants in Quebec City", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "quebec-city", + "ref": "indulge-in-french-canadian-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_indulge-in-french-canadian-cuisine.jpg" + }, + { + "name": "Immerse Yourself in History at the Museum of Civilization", + "description": "Delve into Quebec's rich history and culture at the Museum of Civilization. Explore fascinating exhibits on the region's First Nations people, European settlement, and contemporary society. The museum offers interactive displays and educational programs for all ages.", + "locationName": "Museum of Civilization", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "quebec-city", + "ref": "immerse-yourself-in-history-at-the-museum-of-civilization", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_immerse-yourself-in-history-at-the-museum-of-civilization.jpg" + }, + { + "name": "Hike or Bike the Plains of Abraham", + "description": "Explore the historic Plains of Abraham, a sprawling park where a pivotal battle between the French and British took place in 1759. Enjoy scenic trails for hiking and biking, offering breathtaking views of the St. Lawrence River and the city skyline. In winter, the park transforms into a winter wonderland, perfect for cross-country skiing and snowshoeing.", + "locationName": "Plains of Abraham", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "quebec-city", + "ref": "hike-or-bike-the-plains-of-abraham", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_hike-or-bike-the-plains-of-abraham.jpg" + }, + { + "name": "Discover Local Art at Quartier Petit Champlain", + "description": "Wander through the charming Quartier Petit Champlain, known for its narrow cobblestone streets, unique boutiques, and art galleries. Discover the works of local artists, find one-of-a-kind souvenirs, and soak up the vibrant atmosphere of this historic district.", + "locationName": "Quartier Petit Champlain", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "quebec-city", + "ref": "discover-local-art-at-quartier-petit-champlain", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_discover-local-art-at-quartier-petit-champlain.jpg" + }, + { + "name": "Indulge in Sweet Treats at a Sugar Shack", + "description": "Experience the Quebecois tradition of visiting a sugar shack, especially delightful in the spring during maple syrup season. Enjoy a traditional meal, indulge in maple-infused treats, and learn about the process of making maple syrup.", + "locationName": "Various sugar shacks in the region", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "quebec-city", + "ref": "indulge-in-sweet-treats-at-a-sugar-shack", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_indulge-in-sweet-treats-at-a-sugar-shack.jpg" + }, + { + "name": "Go on a Whale Watching Tour", + "description": "Embark on an unforgettable whale watching excursion from Baie-Sainte-Catherine, a short drive from Quebec City. Witness majestic whales like belugas, humpbacks, and minke whales in their natural habitat, and learn about these fascinating creatures from experienced guides.", + "locationName": "Baie-Sainte-Catherine", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "quebec-city", + "ref": "go-on-a-whale-watching-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_go-on-a-whale-watching-tour.jpg" + }, + { + "name": "Relax at the Nordic Spa", + "description": "Unwind and rejuvenate at the Nordic Spa, a haven of relaxation offering a variety of thermal experiences, including hot and cold pools, saunas, and steam rooms. Enjoy breathtaking views of the St. Lawrence River while indulging in a massage or body treatment.", + "locationName": "Strøm Nordic Spa", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "quebec-city", + "ref": "relax-at-the-nordic-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_relax-at-the-nordic-spa.jpg" + }, + { + "name": "Ice Hotel Experience", + "description": "Embark on a magical winter adventure at the Hôtel de Glace, North America's only ice hotel. Explore the stunning ice sculptures, sip cocktails from ice glasses in the ice bar, or even spend a night in a luxurious ice suite for an unforgettable experience. (Note: This activity is only available during winter months.)", + "locationName": "Hôtel de Glace", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "quebec-city", + "ref": "ice-hotel-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_ice-hotel-experience.jpg" + }, + { + "name": "Montmorency Falls Park", + "description": "Venture outside the city center to witness the breathtaking Montmorency Falls, cascading down 83 meters – taller than Niagara Falls! Take a cable car ride for panoramic views, cross the suspension bridge for an adrenaline rush, or hike the scenic trails surrounding the falls.", + "locationName": "Montmorency Falls Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "quebec-city", + "ref": "montmorency-falls-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_montmorency-falls-park.jpg" + }, + { + "name": "Ile d'Orleans Getaway", + "description": "Escape to the idyllic Ile d'Orleans, a charming island just a short drive from Quebec City. Discover its quaint villages, sample local produce at farms and vineyards, visit historical sites, and enjoy the picturesque landscapes of the St. Lawrence River.", + "locationName": "Ile d'Orleans", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "quebec-city", + "ref": "ile-d-orleans-getaway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_ile-d-orleans-getaway.jpg" + }, + { + "name": "Jacques-Cartier National Park", + "description": "Immerse yourself in the natural beauty of Jacques-Cartier National Park, a paradise for outdoor enthusiasts. Hike through pristine forests, go canoeing or kayaking on the Jacques-Cartier River, spot wildlife, and enjoy breathtaking views of the Laurentian Mountains.", + "locationName": "Jacques-Cartier National Park", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "quebec-city", + "ref": "jacques-cartier-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_jacques-cartier-national-park.jpg" + }, + { + "name": "Fine Dining and Nightlife", + "description": "Experience Quebec City's vibrant culinary scene by indulging in a gourmet dinner at one of its renowned restaurants. Later, explore the city's lively nightlife with options ranging from cozy pubs and live music venues to trendy bars and clubs.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "quebec-city", + "ref": "fine-dining-and-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_fine-dining-and-nightlife.jpg" + }, + { + "name": "Explore the Fortifications of Quebec", + "description": "Embark on a journey through time as you walk along the historic fortifications of Quebec City. These impressive walls, dating back to the 17th century, offer stunning views of the city and the St. Lawrence River. Discover hidden corners, military structures, and historical sites while learning about the city's rich military past.", + "locationName": "Ramparts of Quebec City", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "quebec-city", + "ref": "explore-the-fortifications-of-quebec", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_explore-the-fortifications-of-quebec.jpg" + }, + { + "name": "Visit the Basilica-Cathedral of Notre-Dame de Québec", + "description": "Immerse yourself in the spiritual heart of Quebec City at the Basilica-Cathedral of Notre-Dame de Québec. This architectural gem boasts stunning stained glass windows, intricate sculptures, and a rich history dating back to the 17th century. Attend a mass or simply admire the beauty of this iconic landmark.", + "locationName": "Basilica-Cathedral of Notre-Dame de Québec", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "quebec-city", + "ref": "visit-the-basilica-cathedral-of-notre-dame-de-qu-bec", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_visit-the-basilica-cathedral-of-notre-dame-de-qu-bec.jpg" + }, + { + "name": "Take a Ferry Ride to Lévis", + "description": "Enjoy breathtaking panoramic views of Quebec City's skyline by taking a ferry ride across the St. Lawrence River to Lévis. Capture stunning photos of the city's landmarks, including the Chateau Frontenac and the historic Old Town, from a unique perspective. Explore the charming town of Lévis or simply relax on board and soak in the scenery.", + "locationName": "Quebec City-Lévis Ferry", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "quebec-city", + "ref": "take-a-ferry-ride-to-l-vis", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_take-a-ferry-ride-to-l-vis.jpg" + }, + { + "name": "Wander Through the Quartier des Arts", + "description": "Discover the vibrant artistic scene of Quebec City in the Quartier des Arts. Explore art galleries showcasing local and international artists, catch a live performance at a theater, or simply soak up the bohemian atmosphere. This neighborhood is a hub for creativity and offers a diverse range of artistic experiences.", + "locationName": "Quartier des Arts", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "quebec-city", + "ref": "wander-through-the-quartier-des-arts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_wander-through-the-quartier-des-arts.jpg" + }, + { + "name": "Experience the Magic of the German Christmas Market", + "description": "During the holiday season, immerse yourself in the festive atmosphere of the German Christmas Market. Browse through charming wooden stalls offering traditional German crafts, decorations, and delicious treats. Enjoy live music, sip on mulled wine, and experience the magic of Christmas in a European setting.", + "locationName": "Place de l'Hôtel-de-Ville", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "quebec-city", + "ref": "experience-the-magic-of-the-german-christmas-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/quebec-city_experience-the-magic-of-the-german-christmas-market.jpg" + }, + { + "name": "Hike the Ben Lomond Track", + "description": "Embark on a challenging yet rewarding hike up Ben Lomond, offering breathtaking panoramic views of Queenstown, Lake Wakatipu, and the surrounding mountains. Choose from various trails catering to different fitness levels, from the Tiki Trail to the summit, for an unforgettable experience.", + "locationName": "Ben Lomond Scenic Reserve", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "queenstown", + "ref": "hike-the-ben-lomond-track", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_hike-the-ben-lomond-track.jpg" + }, + { + "name": "Experience the Thrill of Bungy Jumping", + "description": "Take the plunge at the Kawarau Bridge Bungy, the world's first commercial bungy jump, or choose from various other adrenaline-pumping options like the Nevis Bungy and Ledge Bungy. Feel the rush as you leap into the void, surrounded by stunning landscapes.", + "locationName": "Kawarau Bridge, Nevis Bungy, or The Ledge Bungy", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "queenstown", + "ref": "experience-the-thrill-of-bungy-jumping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_experience-the-thrill-of-bungy-jumping.jpg" + }, + { + "name": "Cruise on Lake Wakatipu", + "description": "Enjoy a relaxing and scenic cruise on the pristine waters of Lake Wakatipu aboard the historic TSS Earnslaw steamship. Admire the Remarkables mountain range, visit Walter Peak High Country Farm, and indulge in a delicious gourmet barbecue lunch or dinner.", + "locationName": "Lake Wakatipu", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "queenstown", + "ref": "cruise-on-lake-wakatipu", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_cruise-on-lake-wakatipu.jpg" + }, + { + "name": "Indulge in Local Flavors", + "description": "Explore Queenstown's vibrant culinary scene, from cozy cafes to award-winning restaurants. Sample fresh local produce, savor delicious wines from the Central Otago region, and discover international cuisines. Don't miss the iconic Fergburger for a satisfying burger experience.", + "locationName": "Queenstown Town Center", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "queenstown", + "ref": "indulge-in-local-flavors", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_indulge-in-local-flavors.jpg" + }, + { + "name": "Discover Queenstown's Nightlife", + "description": "As the sun sets, experience Queenstown's vibrant nightlife. Enjoy live music at local pubs, dance the night away at trendy clubs, or relax with a cocktail at a rooftop bar. With a diverse range of options, there's something for everyone to enjoy after dark.", + "locationName": "Queenstown Town Center", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "queenstown", + "ref": "discover-queenstown-s-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_discover-queenstown-s-nightlife.jpg" + }, + { + "name": "Soar Above the Remarkables with a Scenic Flight", + "description": "Experience the breathtaking beauty of Queenstown and the surrounding Southern Alps from a unique perspective. Take a scenic flight over the Remarkables mountain range, glaciers, and Lake Wakatipu, capturing stunning aerial views and creating unforgettable memories. Choose from various flight options, including helicopter tours or fixed-wing aircraft, to tailor your experience.", + "locationName": "Queenstown Airport", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "queenstown", + "ref": "soar-above-the-remarkables-with-a-scenic-flight", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_soar-above-the-remarkables-with-a-scenic-flight.jpg" + }, + { + "name": "Explore the Underwater World with a Dive in Lake Wakatipu", + "description": "Embark on an underwater adventure in the crystal-clear waters of Lake Wakatipu. Discover the unique freshwater marine life and submerged landscapes, including the sunken remains of the historic TSS Earnslaw steamship. Whether you're a seasoned diver or a beginner, local dive operators offer guided experiences for all levels.", + "locationName": "Lake Wakatipu", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "queenstown", + "ref": "explore-the-underwater-world-with-a-dive-in-lake-wakatipu", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_explore-the-underwater-world-with-a-dive-in-lake-wakatipu.jpg" + }, + { + "name": "Unwind and Rejuvenate at Onsen Hot Pools", + "description": "Indulge in a luxurious and relaxing experience at the Onsen Hot Pools. Immerse yourself in the therapeutic waters of private hot tubs perched on a cliffside overlooking the Shotover River. Enjoy breathtaking views of the surrounding mountains while you soak away your stress and rejuvenate your body and mind. Ideal for couples or solo travelers seeking a tranquil escape.", + "locationName": "Onsen Hot Pools", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "queenstown", + "ref": "unwind-and-rejuvenate-at-onsen-hot-pools", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_unwind-and-rejuvenate-at-onsen-hot-pools.jpg" + }, + { + "name": "Journey to Glenorchy and Paradise", + "description": "Take a scenic drive to the picturesque village of Glenorchy, nestled at the northern end of Lake Wakatipu. Explore the stunning landscapes that have served as filming locations for movies like The Lord of the Rings. Continue your journey to Paradise, a remote valley renowned for its untouched beauty and hiking trails. Capture postcard-worthy photos and immerse yourself in the tranquility of nature.", + "locationName": "Glenorchy & Paradise", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "queenstown", + "ref": "journey-to-glenorchy-and-paradise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_journey-to-glenorchy-and-paradise.jpg" + }, + { + "name": "Go White Water Rafting on the Shotover River", + "description": "Get your adrenaline pumping with an exhilarating white-water rafting adventure on the Shotover River. Navigate through thrilling rapids, surrounded by dramatic canyons and stunning scenery. Experienced guides ensure your safety while providing an unforgettable experience for adventure seekers of all levels.", + "locationName": "Shotover River", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "queenstown", + "ref": "go-white-water-rafting-on-the-shotover-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_go-white-water-rafting-on-the-shotover-river.jpg" + }, + { + "name": "Stargazing in the Southern Sky", + "description": "Escape the city lights and embark on a magical stargazing journey. Queenstown's clear skies and minimal light pollution offer breathtaking views of the Milky Way, constellations, and even the Southern Lights. Join a guided tour or find a secluded spot to marvel at the celestial wonders above.", + "locationName": "Queenstown Gardens or Bob's Peak", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "queenstown", + "ref": "stargazing-in-the-southern-sky", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_stargazing-in-the-southern-sky.jpg" + }, + { + "name": "Wine Tour in the Gibbston Valley", + "description": "Embark on a delightful wine tour through the picturesque Gibbston Valley, renowned for its world-class Pinot Noir. Visit boutique wineries, indulge in tastings, and savor gourmet food pairings amidst stunning vineyard landscapes. Learn about the region's unique terroir and the art of winemaking.", + "locationName": "Gibbston Valley", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "queenstown", + "ref": "wine-tour-in-the-gibbston-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_wine-tour-in-the-gibbston-valley.jpg" + }, + { + "name": "Horseback Riding through Paradise", + "description": "Explore the breathtaking landscapes surrounding Queenstown on horseback. Ride through rolling hills, meadows, and alongside crystal-clear rivers, immersing yourself in the tranquility of nature. This unforgettable experience is suitable for all skill levels and offers stunning panoramic views.", + "locationName": "Glenorchy or Paradise Valley", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "queenstown", + "ref": "horseback-riding-through-paradise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_horseback-riding-through-paradise.jpg" + }, + { + "name": "Shop for Unique Souvenirs and Local Crafts", + "description": "Discover Queenstown's vibrant shopping scene, offering a diverse range of unique souvenirs and locally crafted treasures. Explore charming boutiques, art galleries, and craft markets to find the perfect mementos of your trip. From handcrafted jewelry and artwork to merino wool clothing and gourmet treats, there's something for everyone.", + "locationName": "Queenstown Mall or Arrowtown", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "queenstown", + "ref": "shop-for-unique-souvenirs-and-local-crafts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_shop-for-unique-souvenirs-and-local-crafts.jpg" + }, + { + "name": "Take a Scenic Gondola Ride to Bob's Peak", + "description": "Enjoy breathtaking panoramic views of Queenstown and the surrounding mountains with a scenic gondola ride to Bob's Peak. At the top, indulge in delicious cuisine at a mountaintop restaurant, experience the thrill of the Skyline Luge, or simply soak in the awe-inspiring vistas.", + "locationName": "Skyline Queenstown", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "queenstown", + "ref": "take-a-scenic-gondola-ride-to-bob-s-peak", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_take-a-scenic-gondola-ride-to-bob-s-peak.jpg" + }, + { + "name": "Milford Sound Day Trip", + "description": "Embark on a breathtaking journey to Milford Sound, a stunning fiord renowned for its dramatic waterfalls, towering cliffs, and pristine natural beauty. Cruise through the fiord, marvel at the cascading Bowen Falls and Stirling Falls, and keep an eye out for playful dolphins, seals, and penguins. This full-day excursion is a must-do for nature enthusiasts and photographers alike.", + "locationName": "Milford Sound", + "duration": 12, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "queenstown", + "ref": "milford-sound-day-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_milford-sound-day-trip.jpg" + }, + { + "name": "Skyline Luge", + "description": "Get your adrenaline pumping with a thrilling ride on the Skyline Luge! Zoom down the purpose-built track, navigating twists, turns, and tunnels with stunning views of Queenstown and Lake Wakatipu. Choose from different tracks for varying levels of difficulty and enjoy multiple rides to experience the excitement.", + "locationName": "Skyline Queenstown", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "queenstown", + "ref": "skyline-luge", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_skyline-luge.jpg" + }, + { + "name": "Kiwi Birdlife Park", + "description": "Discover New Zealand's unique wildlife at the Kiwi Birdlife Park. Observe the iconic kiwi bird in a nocturnal enclosure, encounter playful kea parrots, and learn about conservation efforts for endangered species. The park offers educational presentations and a chance to see tuataras, geckos, and other native reptiles.", + "locationName": "Kiwi Birdlife Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "queenstown", + "ref": "kiwi-birdlife-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_kiwi-birdlife-park.jpg" + }, + { + "name": "Arrowtown Historic Village", + "description": "Step back in time at Arrowtown, a charming historic gold mining village. Explore the preserved buildings, wander along the quaint streets, and visit the Lakes District Museum to delve into the region's rich history. Pan for gold in the Arrow River, indulge in delicious local fare, and enjoy the picturesque scenery.", + "locationName": "Arrowtown", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "queenstown", + "ref": "arrowtown-historic-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_arrowtown-historic-village.jpg" + }, + { + "name": "Relaxing Spa Day", + "description": "Indulge in a day of pampering and relaxation at one of Queenstown's luxurious spas. Choose from a variety of treatments, including massages, facials, body wraps, and hydrotherapy. Unwind in tranquil surroundings and let the expert therapists melt away your stress and leave you feeling refreshed and rejuvenated.", + "locationName": "Various spas in Queenstown", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "queenstown", + "ref": "relaxing-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/queenstown_relaxing-spa-day.jpg" + }, + { + "name": "Explore the Majestic Mehrangarh Fort", + "description": "Embark on a journey through time at the Mehrangarh Fort, a magnificent 15th-century citadel perched atop a hill overlooking Jodhpur. Marvel at its intricate architecture, explore museums housing royal artifacts, and enjoy panoramic views of the 'Blue City'.", + "locationName": "Jodhpur", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "explore-the-majestic-mehrangarh-fort", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_explore-the-majestic-mehrangarh-fort.jpg" + }, + { + "name": "Camel Safari in the Thar Desert", + "description": "Embark on an unforgettable adventure across the undulating sand dunes of the Thar Desert. Ride atop a camel, witness breathtaking sunsets, and camp under the starry sky for a truly immersive desert experience.", + "locationName": "Jaisalmer", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "rajasthan", + "ref": "camel-safari-in-the-thar-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_camel-safari-in-the-thar-desert.jpg" + }, + { + "name": "Shop at the Bustling Bazaars", + "description": "Immerse yourself in the vibrant atmosphere of Rajasthan's bustling bazaars. Discover a treasure trove of textiles, handicrafts, jewelry, and spices. Hone your bargaining skills and find unique souvenirs to cherish.", + "locationName": "Jaipur", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "shop-at-the-bustling-bazaars", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_shop-at-the-bustling-bazaars.jpg" + }, + { + "name": "Witness the Grandeur of the City Palace", + "description": "Step into the opulent world of Rajasthan's royals at the City Palace in Jaipur. Explore the intricate courtyards, museums, and halls adorned with exquisite artwork and historical artifacts. Get a glimpse into the rich heritage and regal lifestyle of the Maharajas.", + "locationName": "Jaipur", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "witness-the-grandeur-of-the-city-palace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_witness-the-grandeur-of-the-city-palace.jpg" + }, + { + "name": "Experience a Traditional Rajasthani Folk Dance Performance", + "description": "Immerse yourself in the vibrant culture of Rajasthan by witnessing a mesmerizing folk dance performance. Be captivated by the colorful costumes, rhythmic music, and graceful movements that tell stories of the region's rich heritage.", + "locationName": "Udaipur", + "duration": 1.5, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "rajasthan", + "ref": "experience-a-traditional-rajasthani-folk-dance-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_experience-a-traditional-rajasthani-folk-dance-performance.jpg" + }, + { + "name": "Hot Air Balloon Ride Over Jaipur", + "description": "Soar above the Pink City of Jaipur in a hot air balloon and witness the breathtaking landscapes of Rajasthan from a unique perspective. Capture stunning aerial views of majestic forts, palaces, and the sprawling cityscape as you gently drift with the wind. This unforgettable experience offers a serene and romantic way to start your day and create lasting memories.", + "locationName": "Jaipur", + "duration": 1.5, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "rajasthan", + "ref": "hot-air-balloon-ride-over-jaipur", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_hot-air-balloon-ride-over-jaipur.jpg" + }, + { + "name": "Wildlife Safari in Ranthambore National Park", + "description": "Embark on an exhilarating jeep safari through Ranthambore National Park, renowned for its population of Royal Bengal tigers. Spot these majestic creatures in their natural habitat, along with other fascinating wildlife like leopards, sloth bears, crocodiles, and various bird species. Immerse yourself in the beauty of the park's diverse landscapes, from dense forests to tranquil lakes, and experience the thrill of observing animals in the wild.", + "locationName": "Ranthambore National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "rajasthan", + "ref": "wildlife-safari-in-ranthambore-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_wildlife-safari-in-ranthambore-national-park.jpg" + }, + { + "name": "Cooking Class and Traditional Rajasthani Meal", + "description": "Delve into the rich culinary heritage of Rajasthan by participating in a cooking class. Learn the secrets of preparing authentic Rajasthani dishes, from fragrant curries to delectable desserts, under the guidance of a local chef. After your culinary adventure, savor the fruits of your labor with a traditional Rajasthani meal, experiencing the unique flavors and spices of the region.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "cooking-class-and-traditional-rajasthani-meal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_cooking-class-and-traditional-rajasthani-meal.jpg" + }, + { + "name": "Stargazing in the Thar Desert", + "description": "Escape the city lights and venture into the vast Thar Desert for an unforgettable stargazing experience. Lie back under the clear night sky, away from light pollution, and marvel at the countless stars and constellations. Learn about celestial bodies from local guides and enjoy the tranquility of the desert night.", + "locationName": "Thar Desert", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "stargazing-in-the-thar-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_stargazing-in-the-thar-desert.jpg" + }, + { + "name": "Abhaneri Step Well", + "description": "Descend into the architectural marvel of the Chand Baori stepwell in Abhaneri, one of the largest and deepest in India. Marvel at the intricate geometric patterns and the play of light and shadow on the ancient steps.", + "locationName": "Abhaneri", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "abhaneri-step-well", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_abhaneri-step-well.jpg" + }, + { + "name": "Bundi Palace", + "description": "Explore the enchanting Bundi Palace, known for its exquisite murals and miniature paintings. Wander through the halls adorned with vibrant artwork, and enjoy panoramic views of the city and surrounding Aravalli Hills.", + "locationName": "Bundi", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "bundi-palace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_bundi-palace.jpg" + }, + { + "name": "Kumbhalgarh Fort", + "description": "Embark on a journey to Kumbhalgarh Fort, the second-longest wall in the world after the Great Wall of China. Walk along the ramparts, explore the palaces and temples within the fort, and immerse yourself in the historical significance of this UNESCO World Heritage Site.", + "locationName": "Kumbhalgarh", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "rajasthan", + "ref": "kumbhalgarh-fort", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_kumbhalgarh-fort.jpg" + }, + { + "name": "Jaisalmer Desert Festival", + "description": "If you're visiting in February, immerse yourself in the vibrant Jaisalmer Desert Festival. Witness traditional folk dances, camel races, and musical performances against the backdrop of the golden sand dunes.", + "locationName": "Jaisalmer", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "rajasthan", + "ref": "jaisalmer-desert-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_jaisalmer-desert-festival.jpg" + }, + { + "name": "Samode Palace", + "description": "Indulge in luxury and history at the Samode Palace, a heritage hotel known for its exquisite architecture and regal ambiance. Relax by the pool, enjoy a traditional Rajasthani thali, or explore the palace's intricate courtyards and gardens.", + "locationName": "Samode", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "rajasthan", + "ref": "samode-palace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_samode-palace.jpg" + }, + { + "name": "Jeep Safari in the Aravalli Hills", + "description": "Embark on a thrilling jeep safari through the rugged terrain of the Aravalli Hills, the oldest mountain range in India. Discover hidden villages, ancient temples, and breathtaking views of the surrounding landscapes. Keep an eye out for diverse wildlife, including leopards, hyenas, and various bird species. This adventurous experience offers a unique perspective of Rajasthan's natural beauty.", + "locationName": "Aravalli Hills", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "rajasthan", + "ref": "jeep-safari-in-the-aravalli-hills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_jeep-safari-in-the-aravalli-hills.jpg" + }, + { + "name": "Sunset Boat Ride on Lake Pichola", + "description": "As the sun begins its descent, embark on a serene boat ride across the picturesque Lake Pichola in Udaipur. Witness the city's palaces, temples, and ghats bathed in the golden hues of the setting sun. Enjoy the tranquility of the lake and capture stunning photographs of the cityscape. This romantic experience is perfect for couples or those seeking a moment of peace.", + "locationName": "Lake Pichola, Udaipur", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "sunset-boat-ride-on-lake-pichola", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_sunset-boat-ride-on-lake-pichola.jpg" + }, + { + "name": "Village Tour and Rural Life Experience", + "description": "Immerse yourself in the authentic culture of Rajasthan with a visit to a traditional village. Interact with local villagers, learn about their customs and way of life, and participate in daily activities such as pottery making, weaving, or farming. This enriching experience provides a glimpse into the heart and soul of Rajasthan.", + "locationName": "Rural Villages near Jodhpur or Jaipur", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "village-tour-and-rural-life-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_village-tour-and-rural-life-experience.jpg" + }, + { + "name": "Attend a Traditional Puppet Show", + "description": "Experience the magic of Rajasthani puppetry, a centuries-old art form. Watch skilled puppeteers bring colorful puppets to life, narrating folktales and mythological stories. The vibrant costumes, lively music, and engaging performances offer a delightful cultural experience for all ages.", + "locationName": "Jaipur or Jodhpur", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "rajasthan", + "ref": "attend-a-traditional-puppet-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_attend-a-traditional-puppet-show.jpg" + }, + { + "name": "Birdwatching at Keoladeo National Park", + "description": "Discover a haven for birdwatchers at Keoladeo National Park, a UNESCO World Heritage Site. Explore the diverse ecosystems of wetlands, woodlands, and grasslands, home to over 370 bird species. Spot migratory birds, including the Siberian crane, and enjoy the serene natural beauty of the park. This activity is perfect for nature enthusiasts and photography lovers.", + "locationName": "Keoladeo National Park, Bharatpur", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "rajasthan", + "ref": "birdwatching-at-keoladeo-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rajasthan_birdwatching-at-keoladeo-national-park.jpg" + }, + { + "name": "Hike to the Summit of Piton de la Fournaise", + "description": "Embark on an unforgettable adventure to the top of one of the world's most active volcanoes. Witness breathtaking panoramic views of the volcanic landscape, lunar-like craters, and the vast Indian Ocean. This challenging hike is best suited for experienced hikers with a good level of fitness.", + "locationName": "Piton de la Fournaise", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "reunion-island", + "ref": "hike-to-the-summit-of-piton-de-la-fournaise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_hike-to-the-summit-of-piton-de-la-fournaise.jpg" + }, + { + "name": "Relax on the Black Sand Beaches of Etang-Salé", + "description": "Unwind on the unique black sand beaches of Etang-Salé, formed by volcanic activity. Enjoy swimming in the refreshing waters, sunbathing under the tropical sun, and marveling at the dramatic contrast of the black sand against the turquoise ocean.", + "locationName": "Etang-Salé", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "reunion-island", + "ref": "relax-on-the-black-sand-beaches-of-etang-sal-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_relax-on-the-black-sand-beaches-of-etang-sal-.jpg" + }, + { + "name": "Explore the Lush Rainforests of Bébour-Bélouve", + "description": "Immerse yourself in the vibrant biodiversity of Réunion's rainforests. Hike through dense vegetation, discover hidden waterfalls, and encounter unique plant and animal species. This is a perfect activity for nature lovers and those seeking tranquility.", + "locationName": "Bébour-Bélouve Forest", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "reunion-island", + "ref": "explore-the-lush-rainforests-of-b-bour-b-louve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_explore-the-lush-rainforests-of-b-bour-b-louve.jpg" + }, + { + "name": "Discover the Creole Culture in Saint-Denis", + "description": "Explore the charming capital city of Saint-Denis and experience the unique blend of French, African, Indian, and Chinese influences that shape Réunion's Creole culture. Visit historical sites, vibrant markets, and indulge in delicious Creole cuisine.", + "locationName": "Saint-Denis", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "reunion-island", + "ref": "discover-the-creole-culture-in-saint-denis", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_discover-the-creole-culture-in-saint-denis.jpg" + }, + { + "name": "Go Canyoning in the Trou de Fer Canyon", + "description": "Embark on a thrilling canyoning adventure through the spectacular Trou de Fer Canyon. Rappel down cascading waterfalls, swim through crystal-clear pools, and experience the adrenaline rush of this unique activity. This is perfect for adventure seekers and those looking for an unforgettable experience.", + "locationName": "Trou de Fer Canyon", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "reunion-island", + "ref": "go-canyoning-in-the-trou-de-fer-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_go-canyoning-in-the-trou-de-fer-canyon.jpg" + }, + { + "name": "Whale Watching Excursion", + "description": "Embark on a magical boat tour to witness the majestic humpback whales that migrate to Réunion's warm waters between June and October. Watch in awe as these gentle giants breach and slap their tails, creating unforgettable memories.", + "locationName": "Off the coast of Réunion Island", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "reunion-island", + "ref": "whale-watching-excursion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_whale-watching-excursion.jpg" + }, + { + "name": "Helicopter Tour over the Volcanoes", + "description": "Experience the breathtaking volcanic landscapes of Réunion from a unique perspective with a thrilling helicopter tour. Soar above Piton de la Fournaise and the dramatic cirques, capturing stunning aerial views of the island's diverse terrain.", + "locationName": "Departure from Saint-Gilles or Saint-Pierre", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "reunion-island", + "ref": "helicopter-tour-over-the-volcanoes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_helicopter-tour-over-the-volcanoes.jpg" + }, + { + "name": "Explore the Lava Tubes of Saint-Philippe", + "description": "Embark on a spelunking adventure through the fascinating lava tubes formed by past volcanic eruptions. Discover the unique geological formations and learn about the volcanic history of the island.", + "locationName": "Saint-Philippe", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "reunion-island", + "ref": "explore-the-lava-tubes-of-saint-philippe", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_explore-the-lava-tubes-of-saint-philippe.jpg" + }, + { + "name": "Visit the Jardin des Parfums et des Épices", + "description": "Immerse yourself in the fragrant world of spices and perfumes at this botanical garden. Discover a wide variety of exotic plants, learn about their traditional uses, and enjoy the captivating scents of ylang-ylang, vanilla, and other aromatic treasures.", + "locationName": "Saint-Philippe", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "reunion-island", + "ref": "visit-the-jardin-des-parfums-et-des-pices", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_visit-the-jardin-des-parfums-et-des-pices.jpg" + }, + { + "name": "Stargazing on Maïdo Mountain", + "description": "Escape the city lights and venture up Maïdo Mountain for an unforgettable stargazing experience. The high altitude and clear skies offer breathtaking views of the Milky Way and constellations, making it a perfect spot for astronomy enthusiasts.", + "locationName": "Maïdo Mountain", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "reunion-island", + "ref": "stargazing-on-ma-do-mountain", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_stargazing-on-ma-do-mountain.jpg" + }, + { + "name": "Scuba Diving in the Indian Ocean", + "description": "Dive into the crystal-clear waters of the Indian Ocean and discover a vibrant underwater world teeming with marine life. Explore coral reefs, encounter colorful fish, graceful sea turtles, and maybe even dolphins or whales. Réunion Island offers numerous dive sites suitable for all levels, from beginners to experienced divers.", + "locationName": "Various dive sites around the island", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "reunion-island", + "ref": "scuba-diving-in-the-indian-ocean", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_scuba-diving-in-the-indian-ocean.jpg" + }, + { + "name": "Paragliding over the Volcanic Landscape", + "description": "Soar through the sky and witness the breathtaking beauty of Réunion Island's volcanic landscapes from a unique perspective. Paragliding offers an exhilarating experience as you glide over craters, calderas, and lush forests, with panoramic views of the Indian Ocean.", + "locationName": "Saint-Leu or other paragliding launch sites", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "reunion-island", + "ref": "paragliding-over-the-volcanic-landscape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_paragliding-over-the-volcanic-landscape.jpg" + }, + { + "name": "Horseback Riding through the Plaines des Cafres", + "description": "Embark on a scenic horseback riding adventure through the Plaines des Cafres, a vast plateau known for its rolling hills, volcanic craters, and panoramic vistas. This activity allows you to connect with nature and experience the island's diverse landscapes at a leisurely pace.", + "locationName": "Plaines des Cafres", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "reunion-island", + "ref": "horseback-riding-through-the-plaines-des-cafres", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_horseback-riding-through-the-plaines-des-cafres.jpg" + }, + { + "name": "Rum Distillery Tour and Tasting", + "description": "Discover the secrets of rum production on a guided tour of a local distillery. Learn about the history of rum on Réunion Island, witness the distillation process, and indulge in a tasting of various aged rums, savoring the unique flavors of this island specialty.", + "locationName": "Saga du Rhum or other distilleries", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "reunion-island", + "ref": "rum-distillery-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_rum-distillery-tour-and-tasting.jpg" + }, + { + "name": "Market Exploration and Local Cuisine", + "description": "Immerse yourself in the vibrant atmosphere of a local market, such as the Saint-Paul Market. Explore the stalls brimming with fresh produce, exotic spices, handcrafted souvenirs, and local delicacies. Sample Creole dishes, tropical fruits, and other culinary delights, experiencing the authentic flavors of Réunion.", + "locationName": "Saint-Paul Market or other local markets", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "reunion-island", + "ref": "market-exploration-and-local-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_market-exploration-and-local-cuisine.jpg" + }, + { + "name": "Kitesurfing in Saint-Pierre", + "description": "Experience the thrill of kitesurfing in the turquoise waters of Saint-Pierre. With consistent winds and a variety of schools and rental shops available, it's an ideal spot for both beginners and experienced kitesurfers. Soar through the air, ride the waves, and enjoy the stunning coastal scenery.", + "locationName": "Saint-Pierre Beach", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "reunion-island", + "ref": "kitesurfing-in-saint-pierre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_kitesurfing-in-saint-pierre.jpg" + }, + { + "name": "Sunset Cruise along the West Coast", + "description": "Embark on a romantic sunset cruise along Réunion's picturesque west coast. Sail past dramatic cliffs, secluded coves, and charming fishing villages as the sky explodes with vibrant hues. Enjoy breathtaking views, sip on cocktails, and savor a delicious onboard dinner.", + "locationName": "West Coast", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "reunion-island", + "ref": "sunset-cruise-along-the-west-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_sunset-cruise-along-the-west-coast.jpg" + }, + { + "name": "Explore the Underwater World with Snorkeling", + "description": "Discover the vibrant marine life of Réunion through snorkeling. Head to the lagoon of Saint-Gilles-les-Bains or the Hermitage Beach, where calm, clear waters offer excellent visibility. Swim among colorful fish, coral reefs, and other fascinating underwater creatures. A perfect activity for families and nature enthusiasts.", + "locationName": "Saint-Gilles-les-Bains or Hermitage Beach", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "reunion-island", + "ref": "explore-the-underwater-world-with-snorkeling", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_explore-the-underwater-world-with-snorkeling.jpg" + }, + { + "name": "Indulge in a Spa Day at a Luxury Resort", + "description": "Pamper yourself with a rejuvenating spa day at one of Réunion's luxurious resorts. Choose from a range of treatments, including massages, facials, and body wraps, using locally sourced ingredients like volcanic clay and essential oils. Unwind in serene surroundings and emerge feeling refreshed and revitalized.", + "locationName": "Luxury Resorts", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "reunion-island", + "ref": "indulge-in-a-spa-day-at-a-luxury-resort", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_indulge-in-a-spa-day-at-a-luxury-resort.jpg" + }, + { + "name": "Take a Scenic Drive on the Route des Laves", + "description": "Embark on a scenic road trip along the Route des Laves, a coastal road that winds through dramatic volcanic landscapes. Witness the aftermath of past eruptions, marvel at lava flows frozen in time, and enjoy breathtaking views of the Indian Ocean. Stop at viewpoints and explore lava tunnels along the way.", + "locationName": "Route des Laves", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "reunion-island", + "ref": "take-a-scenic-drive-on-the-route-des-laves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/reunion-island_take-a-scenic-drive-on-the-route-des-laves.jpg" + }, + { + "name": "Christ the Redeemer and Corcovado Mountain", + "description": "Ascend Corcovado Mountain to marvel at the iconic Christ the Redeemer statue, a symbol of Rio and one of the New Seven Wonders of the World. Enjoy breathtaking panoramic views of the city, Guanabara Bay, and the surrounding mountains.", + "locationName": "Corcovado Mountain", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "rio-de-janeiro", + "ref": "christ-the-redeemer-and-corcovado-mountain", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_christ-the-redeemer-and-corcovado-mountain.jpg" + }, + { + "name": "Sugarloaf Mountain and Cable Car Ride", + "description": "Take a thrilling cable car ride to the top of Sugarloaf Mountain, another iconic landmark in Rio. Capture stunning views of the city, beaches, and Christ the Redeemer from a different perspective.", + "locationName": "Sugarloaf Mountain", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "rio-de-janeiro", + "ref": "sugarloaf-mountain-and-cable-car-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_sugarloaf-mountain-and-cable-car-ride.jpg" + }, + { + "name": "Copacabana Beach", + "description": "Relax and soak up the sun on the world-famous Copacabana Beach. Take a stroll along the iconic black and white promenade, enjoy a refreshing swim in the ocean, or try your hand at beach volleyball.", + "locationName": "Copacabana Beach", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "rio-de-janeiro", + "ref": "copacabana-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_copacabana-beach.jpg" + }, + { + "name": "Carnival Experience", + "description": "Immerse yourself in the vibrant and energetic atmosphere of Rio's Carnival. Watch the spectacular parades with elaborate costumes and samba dancing, or join a street party and dance the night away.", + "locationName": "Sambadrome or various neighborhoods", + "duration": 5, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "rio-de-janeiro", + "ref": "carnival-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_carnival-experience.jpg" + }, + { + "name": "Tijuca National Park Hike", + "description": "Explore the Tijuca National Park, the world's largest urban rainforest. Hike through lush trails, discover hidden waterfalls, and encounter diverse wildlife.", + "locationName": "Tijuca National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "rio-de-janeiro", + "ref": "tijuca-national-park-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_tijuca-national-park-hike.jpg" + }, + { + "name": "Hang Gliding over Rio", + "description": "Experience breathtaking panoramic views of Rio's iconic landmarks and stunning coastline as you soar through the sky on a tandem hang gliding adventure. Take off from Pedra Bonita ramp and glide with experienced instructors, feeling the adrenaline rush as you witness the city's beauty from a unique perspective. This exhilarating activity is perfect for thrill-seekers and offers unforgettable memories.", + "locationName": "Pedra Bonita", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "rio-de-janeiro", + "ref": "hang-gliding-over-rio", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_hang-gliding-over-rio.jpg" + }, + { + "name": "Explore Santa Teresa", + "description": "Wander through the charming bohemian neighborhood of Santa Teresa, known for its artistic vibe, historic tram, and colorful houses. Discover local art galleries, studios, and craft shops, and enjoy the relaxed atmosphere of this hillside enclave. Take a ride on the iconic yellow tram for a scenic journey and visit Parque das Ruínas, a cultural center with stunning city views.", + "locationName": "Santa Teresa", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "rio-de-janeiro", + "ref": "explore-santa-teresa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_explore-santa-teresa.jpg" + }, + { + "name": "Visit the Maracanã Stadium", + "description": "Immerse yourself in Brazil's passion for football with a tour of the legendary Maracanã Stadium. Explore the history of this iconic venue, which hosted the FIFA World Cup finals in 1950 and 2014, and learn about its significance in Brazilian culture. Walk through the players' tunnel, visit the locker rooms, and imagine the electrifying atmosphere during a match.", + "locationName": "Maracanã Stadium", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "rio-de-janeiro", + "ref": "visit-the-maracan-stadium", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_visit-the-maracan-stadium.jpg" + }, + { + "name": "Experience Samba Nightlife", + "description": "Immerse yourself in the vibrant nightlife of Rio by attending a Samba show. Head to a local Samba club or a dedicated performance venue and enjoy the energetic music, captivating dance routines, and lively atmosphere. Feel the rhythm of Rio's soul as you witness the passion and talent of Samba dancers and musicians, creating an unforgettable cultural experience.", + "locationName": "Rio Scenarium (or other Samba clubs)", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "rio-de-janeiro", + "ref": "experience-samba-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_experience-samba-nightlife.jpg" + }, + { + "name": "Botanical Garden Exploration", + "description": "Escape the city's hustle and bustle with a visit to the Rio de Janeiro Botanical Garden. Explore the diverse collection of plants and flowers from around the world, including orchids, bromeliads, and giant water lilies. Enjoy a peaceful stroll through the gardens, visit the sensory garden, and discover the historical significance of this green oasis.", + "locationName": "Rio de Janeiro Botanical Garden", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "rio-de-janeiro", + "ref": "botanical-garden-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_botanical-garden-exploration.jpg" + }, + { + "name": "Island Escape to Ilha Fiscal", + "description": "Embark on a ferry trip to the enchanting Ilha Fiscal, a small island in Guanabara Bay known for its fairytale-like palace. Explore the opulent halls and gardens of the former customs house, delve into its fascinating history, and enjoy breathtaking panoramic views of the city.", + "locationName": "Ilha Fiscal", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "rio-de-janeiro", + "ref": "island-escape-to-ilha-fiscal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_island-escape-to-ilha-fiscal.jpg" + }, + { + "name": "Pedra da Gávea Hike and Climb", + "description": "For adventurous souls, the Pedra da Gávea hike offers a challenging but rewarding experience. Trek through lush rainforest, scramble up rocky slopes, and conquer the iconic 'Carrasqueira' rock face. The summit rewards you with awe-inspiring vistas of the entire city and coastline.", + "locationName": "Pedra da Gávea", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "rio-de-janeiro", + "ref": "pedra-da-g-vea-hike-and-climb", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_pedra-da-g-vea-hike-and-climb.jpg" + }, + { + "name": "Bohemian Vibes in Lapa", + "description": "Immerse yourself in the vibrant nightlife of Lapa, Rio's bohemian district. Wander through the colorful streets, admire the Arcos da Lapa, and hop between lively bars and clubs pulsating with samba, choro, and other Brazilian rhythms.", + "locationName": "Lapa", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "rio-de-janeiro", + "ref": "bohemian-vibes-in-lapa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_bohemian-vibes-in-lapa.jpg" + }, + { + "name": "Sailboat Cruise on Guanabara Bay", + "description": "Experience the beauty of Rio from a different perspective with a relaxing sailboat cruise on Guanabara Bay. Soak up the sun, admire the iconic landmarks from the water, and enjoy the refreshing sea breeze. Opt for a sunset cruise for a truly magical experience.", + "locationName": "Guanabara Bay", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "rio-de-janeiro", + "ref": "sailboat-cruise-on-guanabara-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_sailboat-cruise-on-guanabara-bay.jpg" + }, + { + "name": "Feira Hippie de Ipanema: Treasure Hunt", + "description": "Discover unique souvenirs and local crafts at the Feira Hippie de Ipanema, a vibrant open-air market. Browse through stalls filled with handmade jewelry, clothing, art, and more. Enjoy the lively atmosphere and find the perfect memento of your Rio adventure.", + "locationName": "Ipanema", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "rio-de-janeiro", + "ref": "feira-hippie-de-ipanema-treasure-hunt", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_feira-hippie-de-ipanema-treasure-hunt.jpg" + }, + { + "name": "Museum of Tomorrow", + "description": "Embark on a journey into the future at the Museum of Tomorrow, a science museum with interactive exhibits exploring sustainability and the challenges facing our planet. The striking architecture and thought-provoking displays make it a unique and educational experience for all ages.", + "locationName": "Museum of Tomorrow", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "rio-de-janeiro", + "ref": "museum-of-tomorrow", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_museum-of-tomorrow.jpg" + }, + { + "name": "Parque Lage", + "description": "Escape the city bustle at Parque Lage, a tranquil oasis with a historic mansion, a picturesque lake, and lush gardens. Enjoy a leisurely stroll, visit the art school, or simply relax at the on-site cafe and soak in the serene atmosphere. ", + "locationName": "Parque Lage", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "rio-de-janeiro", + "ref": "parque-lage", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_parque-lage.jpg" + }, + { + "name": "AquaRio", + "description": "Dive into the underwater world at AquaRio, the largest marine aquarium in South America. Discover a diverse array of marine life, from colorful fish to majestic sharks, and walk through a mesmerizing underwater tunnel for an immersive experience.", + "locationName": "AquaRio", + "duration": 2.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "rio-de-janeiro", + "ref": "aquario", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_aquario.jpg" + }, + { + "name": "Theatro Municipal do Rio de Janeiro", + "description": "Experience the grandeur of Theatro Municipal do Rio de Janeiro, a stunning opera house renowned for its opulent architecture and world-class performances. Catch a ballet, opera, or symphony concert for a dose of culture and sophistication.", + "locationName": "Theatro Municipal do Rio de Janeiro", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "rio-de-janeiro", + "ref": "theatro-municipal-do-rio-de-janeiro", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_theatro-municipal-do-rio-de-janeiro.jpg" + }, + { + "name": "Ilha da Gigóia", + "description": "Escape the city and explore Ilha da Gigóia, a car-free island in the Barra da Tijuca neighborhood. Rent a kayak or stand-up paddleboard, enjoy fresh seafood at a local restaurant, and discover the island's laid-back charm.", + "locationName": "Ilha da Gigóia", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "rio-de-janeiro", + "ref": "ilha-da-gig-ia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/rio-de-janeiro_ilha-da-gig-ia.jpg" + }, + { + "name": "Salt Flats Photography Tour", + "description": "Embark on a guided photography tour across the mesmerizing Salar de Uyuni. Capture the surreal reflections of the sky on the flooded salt flats during the rainy season, creating stunning optical illusions. Learn perspective tricks from your guide to take playful and unforgettable photos with the unique landscape.", + "locationName": "Salar de Uyuni", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "salar-de-uyuni", + "ref": "salt-flats-photography-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_salt-flats-photography-tour.jpg" + }, + { + "name": "Stargazing Experience", + "description": "Witness the breathtaking spectacle of the night sky above the Salar de Uyuni. Far from city lights, the desert offers unparalleled views of the Milky Way and constellations. Join a stargazing tour led by an expert guide who will share insights about the celestial wonders above.", + "locationName": "Salar de Uyuni", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "salar-de-uyuni", + "ref": "stargazing-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_stargazing-experience.jpg" + }, + { + "name": "Isla Incahuasi Exploration", + "description": "Venture to Isla Incahuasi, an island oasis in the heart of the salt flats. Hike to the island's summit for panoramic views of the vast white expanse. Explore the unique ecosystem with its giant cacti, and learn about the island's history and geological formations.", + "locationName": "Isla Incahuasi", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "salar-de-uyuni", + "ref": "isla-incahuasi-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_isla-incahuasi-exploration.jpg" + }, + { + "name": "Train Cemetery Visit", + "description": "Step back in time at the Train Cemetery, a collection of rusting locomotives and train cars from Bolivia's mining era. Explore the abandoned trains, capture unique photos, and learn about the history of the railway system and its impact on the region.", + "locationName": "Uyuni Train Cemetery", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "salar-de-uyuni", + "ref": "train-cemetery-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_train-cemetery-visit.jpg" + }, + { + "name": "Laguna Colorada Flamingo Viewing", + "description": "Journey to Laguna Colorada, a vibrant red lagoon known for its large population of flamingos. Observe these elegant birds in their natural habitat as they feed and interact. Learn about the unique ecosystem of the lagoon and the factors that contribute to its distinctive color.", + "locationName": "Laguna Colorada", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "salar-de-uyuni", + "ref": "laguna-colorada-flamingo-viewing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_laguna-colorada-flamingo-viewing.jpg" + }, + { + "name": "Andean Salt Hotel Experience", + "description": "Spend a night in a unique hotel constructed entirely from salt blocks, offering an unforgettable experience in the heart of the Salar de Uyuni. Marvel at the intricate salt carvings, enjoy local cuisine in the salt restaurant, and relax in the cozy atmosphere of this architectural wonder.", + "locationName": "Salt Hotel on the Salar de Uyuni", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "salar-de-uyuni", + "ref": "andean-salt-hotel-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_andean-salt-hotel-experience.jpg" + }, + { + "name": "Off-Road Adventure to the Tunupa Volcano", + "description": "Embark on an exhilarating 4x4 journey to the Tunupa Volcano, a dormant stratovolcano offering breathtaking panoramic views of the Salar de Uyuni and surrounding landscapes. Explore ancient caves adorned with cave paintings, hike to the volcano's crater, and learn about the region's geological history.", + "locationName": "Tunupa Volcano", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "salar-de-uyuni", + "ref": "off-road-adventure-to-the-tunupa-volcano", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_off-road-adventure-to-the-tunupa-volcano.jpg" + }, + { + "name": "Mountain Biking on the Salt Flats", + "description": "Experience the vastness of the Salar de Uyuni on two wheels, cycling across the seemingly endless expanse of salt. Feel the cool breeze against your face as you pedal through this unique landscape, stopping to capture stunning photos and enjoy the serene atmosphere.", + "locationName": "Salar de Uyuni", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "salar-de-uyuni", + "ref": "mountain-biking-on-the-salt-flats", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_mountain-biking-on-the-salt-flats.jpg" + }, + { + "name": "Sunset Picnic on the Salt Flats", + "description": "Indulge in a romantic and unforgettable experience with a sunset picnic on the Salar de Uyuni. As the sun dips below the horizon, witness the sky ablaze with vibrant colors, reflecting off the shimmering salt crust. Enjoy a delicious spread of local delicacies and savor the magical ambiance.", + "locationName": "Salar de Uyuni", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "salar-de-uyuni", + "ref": "sunset-picnic-on-the-salt-flats", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_sunset-picnic-on-the-salt-flats.jpg" + }, + { + "name": "Uyuni Market Exploration", + "description": "Immerse yourself in the local culture with a visit to the vibrant Uyuni Market. Browse through stalls brimming with colorful handicrafts, textiles, and souvenirs. Sample regional delicacies, interact with friendly vendors, and discover the authentic flavors of Bolivia.", + "locationName": "Uyuni Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "salar-de-uyuni", + "ref": "uyuni-market-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_uyuni-market-exploration.jpg" + }, + { + "name": "Cave Exploration at Cueva del Diablo", + "description": "Embark on a thrilling adventure to the 'Devil's Cave,' a natural cavern located near the salt flats. Explore its eerie chambers adorned with stalactites and stalagmites, and learn about the local legends and folklore surrounding this mysterious site.", + "locationName": "Cueva del Diablo", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "salar-de-uyuni", + "ref": "cave-exploration-at-cueva-del-diablo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_cave-exploration-at-cueva-del-diablo.jpg" + }, + { + "name": "Authentic Quinoa Farm Visit and Lunch", + "description": "Immerse yourself in the local culture with a visit to a traditional quinoa farm. Learn about the cultivation process of this Andean superfood, participate in harvesting activities (depending on the season), and savor a delicious lunch prepared with fresh, locally-sourced ingredients.", + "locationName": "Local quinoa farm", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "salar-de-uyuni", + "ref": "authentic-quinoa-farm-visit-and-lunch", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_authentic-quinoa-farm-visit-and-lunch.jpg" + }, + { + "name": "Sunrise Hot Springs Experience", + "description": "Start your day with a magical sunrise soak in the natural hot springs near the salt flats. Relax and rejuvenate in the therapeutic waters while witnessing the breathtaking colors of the dawn sky. This is a perfect opportunity for stunning photographs and peaceful reflection.", + "locationName": "Hot springs near Salar de Uyuni", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "salar-de-uyuni", + "ref": "sunrise-hot-springs-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_sunrise-hot-springs-experience.jpg" + }, + { + "name": "Local Village Cultural Exchange", + "description": "Engage with the local community and experience their way of life. Visit a nearby village, interact with residents, learn about their customs and traditions, and perhaps even participate in a traditional craft workshop.", + "locationName": "Local village", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "salar-de-uyuni", + "ref": "local-village-cultural-exchange", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_local-village-cultural-exchange.jpg" + }, + { + "name": "Andean Textile Art Workshop", + "description": "Discover the vibrant world of Andean textiles with a hands-on workshop. Learn about traditional weaving techniques, natural dyes, and the cultural significance of these intricate textiles. Create your own small piece of art to take home as a unique souvenir.", + "locationName": "Local workshop or community center", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "salar-de-uyuni", + "ref": "andean-textile-art-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_andean-textile-art-workshop.jpg" + }, + { + "name": "Salt Flat ATV Adventure", + "description": "Embark on an exhilarating ATV adventure across the vast expanse of the Salar de Uyuni. Feel the wind in your hair as you zoom past unique rock formations, hidden lagoons, and endless white landscapes. This thrilling experience offers a unique perspective of the salt flats and is perfect for adrenaline seekers.", + "locationName": "Salar de Uyuni", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "salar-de-uyuni", + "ref": "salt-flat-atv-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_salt-flat-atv-adventure.jpg" + }, + { + "name": "Andean Astronomy Night", + "description": "Experience the magic of the Uyuni night sky with a guided astronomy tour. Far from city lights, the salt flats offer unparalleled views of the Milky Way and constellations. Learn about Andean astronomy, mythology, and the significance of the stars in the local culture. This activity is perfect for couples, families, and anyone seeking a unique and awe-inspiring experience.", + "locationName": "Salar de Uyuni", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "salar-de-uyuni", + "ref": "andean-astronomy-night", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_andean-astronomy-night.jpg" + }, + { + "name": "Traditional Salt Harvesting Experience", + "description": "Discover the ancient art of salt harvesting with a visit to a local salt mining community. Learn about the traditional methods used to extract salt from the flats and the cultural significance of this practice. Participate in the process, from breaking the salt crust to loading it onto trucks. This immersive experience offers a glimpse into the lives of the local people and their connection to the salt flats.", + "locationName": "Colchani Village", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "salar-de-uyuni", + "ref": "traditional-salt-harvesting-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_traditional-salt-harvesting-experience.jpg" + }, + { + "name": "Uyuni Photography Workshop", + "description": "Elevate your photography skills with a specialized workshop on the Salar de Uyuni. Learn about composition, lighting, and perspective from experienced photographers, capturing the unique beauty of the salt flats. This workshop is perfect for photography enthusiasts of all levels and will help you create stunning images to remember your trip.", + "locationName": "Salar de Uyuni", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "salar-de-uyuni", + "ref": "uyuni-photography-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_uyuni-photography-workshop.jpg" + }, + { + "name": "Andean Music and Dance Performance", + "description": "Immerse yourself in the vibrant culture of the Andes with a traditional music and dance performance. Enjoy the lively rhythms of local instruments and witness the colorful costumes and energetic dances. This cultural experience provides insight into the rich heritage of the region and is a perfect way to end a day on the salt flats.", + "locationName": "Uyuni Town", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "salar-de-uyuni", + "ref": "andean-music-and-dance-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/salar-de-uyuni_andean-music-and-dance-performance.jpg" + }, + { + "name": "Explore Balboa Park", + "description": "Spend a day wandering through the beautiful Balboa Park, a cultural oasis with stunning gardens, museums, theaters, and the world-famous San Diego Zoo. Discover diverse plant life in the Botanical Building, marvel at Spanish Renaissance architecture, and enjoy street performances.", + "locationName": "Balboa Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-diego", + "ref": "explore-balboa-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_explore-balboa-park.jpg" + }, + { + "name": "Visit the San Diego Zoo", + "description": "Embark on a wildlife adventure at the renowned San Diego Zoo, home to over 3,500 animals from around the world. See giant pandas, koalas, elephants, and more in naturalistic habitats. Take a guided bus tour, ride the aerial tram, and enjoy educational shows.", + "locationName": "San Diego Zoo", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-diego", + "ref": "visit-the-san-diego-zoo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_visit-the-san-diego-zoo.jpg" + }, + { + "name": "Relax on Coronado Island", + "description": "Escape to the picturesque Coronado Island, known for its pristine beaches, charming shops, and the iconic Hotel del Coronado. Sunbathe on the soft sand, try water sports like kayaking or paddleboarding, and enjoy a leisurely bike ride along the coast.", + "locationName": "Coronado Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-diego", + "ref": "relax-on-coronado-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_relax-on-coronado-island.jpg" + }, + { + "name": "Discover the Gaslamp Quarter", + "description": "Step back in time in the historic Gaslamp Quarter, a vibrant district with Victorian-era architecture, trendy restaurants, and lively nightlife. Explore art galleries, enjoy live music at a jazz bar, and experience the city's energetic atmosphere.", + "locationName": "Gaslamp Quarter", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "san-diego", + "ref": "discover-the-gaslamp-quarter", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_discover-the-gaslamp-quarter.jpg" + }, + { + "name": "Catch a wave at La Jolla Shores", + "description": "Head to La Jolla Shores, a popular beach known for its gentle waves, making it perfect for beginner surfers and families. Take a surf lesson, rent a board, or simply relax on the sand and watch the surfers. Explore the nearby La Jolla Underwater Park for snorkeling and diving adventures.", + "locationName": "La Jolla Shores", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-diego", + "ref": "catch-a-wave-at-la-jolla-shores", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_catch-a-wave-at-la-jolla-shores.jpg" + }, + { + "name": "Kayak with Sea Lions in La Jolla Cove", + "description": "Embark on a guided kayaking tour of the La Jolla sea caves and ecological reserve. Paddle alongside playful sea lions, marvel at the dramatic cliffs, and discover hidden coves. This eco-friendly adventure offers stunning views and close encounters with marine life.", + "locationName": "La Jolla Cove", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-diego", + "ref": "kayak-with-sea-lions-in-la-jolla-cove", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_kayak-with-sea-lions-in-la-jolla-cove.jpg" + }, + { + "name": "Hike Torrey Pines State Natural Reserve", + "description": "Experience the breathtaking beauty of Torrey Pines State Natural Reserve, with its iconic sandstone cliffs overlooking the Pacific Ocean. Hike through trails offering panoramic ocean views, diverse plant life, and opportunities for bird watching. Choose from various trails suitable for different fitness levels.", + "locationName": "Torrey Pines State Natural Reserve", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-diego", + "ref": "hike-torrey-pines-state-natural-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_hike-torrey-pines-state-natural-reserve.jpg" + }, + { + "name": "Explore the San Diego Harbor by Boat", + "description": "Enjoy a scenic cruise or sailing adventure around San Diego Harbor. Admire the city skyline, the iconic Coronado Bridge, and naval ships. Opt for a sunset cruise with breathtaking views or a whale watching tour during migration season.", + "locationName": "San Diego Harbor", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-diego", + "ref": "explore-the-san-diego-harbor-by-boat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_explore-the-san-diego-harbor-by-boat.jpg" + }, + { + "name": "Catch a Show at the Old Globe Theatre", + "description": "Experience world-class theater at the Old Globe, renowned for its Shakespearean productions and Broadway-caliber shows. Choose from a variety of performances in different theaters, enjoying a captivating evening of entertainment in a beautiful setting.", + "locationName": "Old Globe Theatre", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "san-diego", + "ref": "catch-a-show-at-the-old-globe-theatre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_catch-a-show-at-the-old-globe-theatre.jpg" + }, + { + "name": "Go tidepooling at Cabrillo National Monument", + "description": "Discover the fascinating world of tide pools at Cabrillo National Monument, where you can observe sea creatures like starfish, anemones, and crabs in their natural habitat. This educational and engaging activity is perfect for families and nature enthusiasts.", + "locationName": "Cabrillo National Monument", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-diego", + "ref": "go-tidepooling-at-cabrillo-national-monument", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_go-tidepooling-at-cabrillo-national-monument.jpg" + }, + { + "name": "Stroll through the San Diego Botanic Garden", + "description": "Immerse yourself in the beauty of diverse plant life at the San Diego Botanic Garden. Explore themed gardens, including a desert garden, a tropical rainforest, and a California native plant section. Enjoy a peaceful escape surrounded by nature's wonders.", + "locationName": "San Diego Botanic Garden", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-diego", + "ref": "stroll-through-the-san-diego-botanic-garden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_stroll-through-the-san-diego-botanic-garden.jpg" + }, + { + "name": "Take a day trip to Julian", + "description": "Escape the city and venture into the charming mountain town of Julian. Known for its apple pies and historic gold mining past, Julian offers a delightful change of pace. Explore the town's shops, enjoy a slice of pie, and hike amidst scenic landscapes.", + "locationName": "Julian", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-diego", + "ref": "take-a-day-trip-to-julian", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_take-a-day-trip-to-julian.jpg" + }, + { + "name": "Visit the Birch Aquarium at Scripps", + "description": "Delve into the wonders of the underwater world at the Birch Aquarium at Scripps. Explore exhibits showcasing diverse marine life, learn about oceanographic research, and enjoy breathtaking views of the Pacific Ocean.", + "locationName": "Birch Aquarium at Scripps", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-diego", + "ref": "visit-the-birch-aquarium-at-scripps", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_visit-the-birch-aquarium-at-scripps.jpg" + }, + { + "name": "Embark on a Whale Watching Adventure", + "description": "Experience the thrill of witnessing majestic whales in their natural habitat on a whale watching excursion. San Diego is renowned as a prime location for spotting gray whales during their annual migration, as well as other whale species like humpbacks and blue whales. Join a guided tour and marvel at these gentle giants as they breach, spout, and swim alongside the boat.", + "locationName": "San Diego Harbor", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-diego", + "ref": "embark-on-a-whale-watching-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_embark-on-a-whale-watching-adventure.jpg" + }, + { + "name": "Indulge in a Craft Beer Tour", + "description": "Discover San Diego's thriving craft beer scene by embarking on a brewery tour. With numerous award-winning breweries scattered throughout the city, you can sample a wide variety of beers, from hoppy IPAs to rich stouts. Join a guided tour to learn about the brewing process, meet local brewers, and indulge in delicious beer tastings.", + "locationName": "Various breweries throughout San Diego", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "san-diego", + "ref": "indulge-in-a-craft-beer-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_indulge-in-a-craft-beer-tour.jpg" + }, + { + "name": "Explore the Vibrant Neighborhoods", + "description": "Beyond the popular tourist spots, San Diego boasts diverse and charming neighborhoods, each with its unique character. Explore the historic streets of Old Town San Diego, experience the hipster vibes of North Park, or discover the artistic spirit of Barrio Logan. Each neighborhood offers a distinct atmosphere, local shops, restaurants, and cultural attractions.", + "locationName": "Various neighborhoods throughout San Diego", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-diego", + "ref": "explore-the-vibrant-neighborhoods", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_explore-the-vibrant-neighborhoods.jpg" + }, + { + "name": "Take a Day Trip to Tijuana, Mexico", + "description": "Just a short drive from San Diego lies Tijuana, Mexico, offering a vibrant cultural experience. Explore the bustling Avenida Revolución, sample authentic Mexican cuisine, discover local artisan crafts, and immerse yourself in the lively atmosphere of this border city.", + "locationName": "Tijuana, Mexico", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-diego", + "ref": "take-a-day-trip-to-tijuana-mexico", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_take-a-day-trip-to-tijuana-mexico.jpg" + }, + { + "name": "Go Hiking or Biking with Scenic Views", + "description": "San Diego offers numerous hiking and biking trails with breathtaking views. Explore the coastal trails of Torrey Pines State Natural Reserve, hike to the summit of Cowles Mountain for panoramic vistas, or bike along the scenic Mission Beach Boardwalk. Enjoy the fresh air, exercise, and stunning natural landscapes.", + "locationName": "Various trails throughout San Diego", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-diego", + "ref": "go-hiking-or-biking-with-scenic-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-diego_go-hiking-or-biking-with-scenic-views.jpg" + }, + { + "name": "Explore the Historic Centro", + "description": "Wander through the cobblestone streets of San Miguel's historic center, admiring the colonial architecture, vibrant colors, and charming atmosphere. Visit the iconic Parroquia de San Miguel Arcángel, a pink neo-Gothic church, and explore the many art galleries, boutiques, and cafes.", + "locationName": "Centro Histórico", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-miguel-de-allende", + "ref": "explore-the-historic-centro", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_explore-the-historic-centro.jpg" + }, + { + "name": "Immerse in Art and Culture", + "description": "San Miguel de Allende is an art lover's paradise. Visit the Fabrica La Aurora, a former textile factory turned art and design center, or explore the many art galleries showcasing works by local and international artists. Take an art class or workshop to unleash your creativity.", + "locationName": "Fabrica La Aurora, art galleries throughout the city", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-miguel-de-allende", + "ref": "immerse-in-art-and-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_immerse-in-art-and-culture.jpg" + }, + { + "name": "Savor Culinary Delights", + "description": "Indulge in San Miguel's vibrant culinary scene. Sample traditional Mexican dishes at local restaurants, enjoy fine dining experiences with international flavors, or take a cooking class to learn the secrets of Mexican cuisine. Don't miss the opportunity to try the city's famous churros and mole.", + "locationName": "Various restaurants and cooking schools throughout the city", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-miguel-de-allende", + "ref": "savor-culinary-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_savor-culinary-delights.jpg" + }, + { + "name": "Experience the Rooftop Views", + "description": "Enjoy breathtaking panoramic views of the city from one of San Miguel's many rooftop bars and restaurants. Sip on a margarita, admire the sunset over the colorful cityscape, and soak up the magical ambiance of this charming colonial city.", + "locationName": "Various rooftop venues throughout the city", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 3, + "destinationRef": "san-miguel-de-allende", + "ref": "experience-the-rooftop-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_experience-the-rooftop-views.jpg" + }, + { + "name": "Day Trip to the Sanctuary of Atotonilco", + "description": "Embark on a cultural excursion to the Sanctuary of Atotonilco, a UNESCO World Heritage site known as the 'Sistine Chapel of Mexico.' Admire the impressive murals and architecture of this 18th-century religious complex, and learn about its historical and spiritual significance.", + "locationName": "Sanctuary of Atotonilco", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-miguel-de-allende", + "ref": "day-trip-to-the-sanctuary-of-atotonilco", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_day-trip-to-the-sanctuary-of-atotonilco.jpg" + }, + { + "name": "Horseback Riding in the Countryside", + "description": "Embark on a scenic horseback riding adventure through the picturesque countryside surrounding San Miguel de Allende. Several ranches and tour operators offer guided horseback riding excursions, allowing you to explore the rolling hills, charming villages, and breathtaking landscapes of the region. Whether you're an experienced rider or a beginner, this activity provides a unique perspective and a chance to connect with nature.", + "locationName": "Countryside surrounding San Miguel de Allende", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-miguel-de-allende", + "ref": "horseback-riding-in-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_horseback-riding-in-the-countryside.jpg" + }, + { + "name": "Hot Air Balloon Ride over the City", + "description": "Experience the magic of San Miguel de Allende from a breathtaking perspective with a hot air balloon ride. Ascend into the sky and marvel at the panoramic views of the city's colonial architecture, colorful streets, and surrounding landscapes. Enjoy the tranquility of floating above the rooftops and capture unforgettable memories as the sun rises or sets.", + "locationName": "San Miguel de Allende", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "san-miguel-de-allende", + "ref": "hot-air-balloon-ride-over-the-city", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_hot-air-balloon-ride-over-the-city.jpg" + }, + { + "name": "Relax and Rejuvenate at a Spa", + "description": "Indulge in a pampering spa experience and rejuvenate your mind, body, and soul. San Miguel de Allende offers a variety of luxurious spas and wellness centers where you can enjoy a range of treatments, including massages, facials, body wraps, and hydrotherapy. Immerse yourself in a tranquil atmosphere and let the stresses of everyday life melt away.", + "locationName": "Various spas and wellness centers in San Miguel de Allende", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "san-miguel-de-allende", + "ref": "relax-and-rejuvenate-at-a-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_relax-and-rejuvenate-at-a-spa.jpg" + }, + { + "name": "Explore the Fabrica La Aurora", + "description": "Visit the Fabrica La Aurora, a former textile factory that has been transformed into a vibrant art and design center. Explore the various galleries, studios, and shops showcasing the works of local and international artists. Discover unique paintings, sculptures, jewelry, furniture, and more. The Fabrica La Aurora also features charming cafes and restaurants where you can enjoy a leisurely meal or a cup of coffee.", + "locationName": "Fabrica La Aurora", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-miguel-de-allende", + "ref": "explore-the-fabrica-la-aurora", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_explore-the-fabrica-la-aurora.jpg" + }, + { + "name": "Take a Cooking Class", + "description": "Immerse yourself in Mexican culinary culture by taking a cooking class. Learn how to prepare traditional dishes from local chefs, using fresh, regional ingredients. Discover the secrets of Mexican cuisine, from classic recipes to contemporary creations. Many cooking classes also include a visit to a local market to select ingredients and learn about Mexican culinary traditions.", + "locationName": "Various cooking schools and culinary centers in San Miguel de Allende", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-miguel-de-allende", + "ref": "take-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_take-a-cooking-class.jpg" + }, + { + "name": "Hiking in the Canyon", + "description": "Embark on a scenic hike through the breathtaking canyon just outside San Miguel de Allende. Explore the rugged trails, surrounded by stunning rock formations and panoramic views of the city. Keep an eye out for local flora and fauna along the way, and enjoy a picnic lunch amidst nature's beauty.", + "locationName": "Canyon outside San Miguel de Allende", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-miguel-de-allende", + "ref": "hiking-in-the-canyon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_hiking-in-the-canyon.jpg" + }, + { + "name": "Live Music at a Local Bar", + "description": "Immerse yourself in the vibrant nightlife of San Miguel de Allende by catching a live music performance at a local bar. From traditional Mexican folk music to contemporary jazz and blues, the city offers a diverse range of musical experiences to suit all tastes. Enjoy a margarita or local craft beer as you soak up the lively atmosphere.", + "locationName": "Various bars in San Miguel de Allende", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "san-miguel-de-allende", + "ref": "live-music-at-a-local-bar", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_live-music-at-a-local-bar.jpg" + }, + { + "name": "Visit the Botanical Garden", + "description": "Escape the hustle and bustle of the city with a visit to the serene Botanical Garden. Stroll through a diverse collection of cacti, succulents, and other native plants, and learn about the region's unique flora. Enjoy the peaceful ambiance and take in the stunning views of the surrounding landscape.", + "locationName": "El Charco del Ingenio Botanical Garden", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-miguel-de-allende", + "ref": "visit-the-botanical-garden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_visit-the-botanical-garden.jpg" + }, + { + "name": "Shop for Unique Souvenirs", + "description": "Wander through the charming streets of San Miguel de Allende and discover a treasure trove of unique souvenirs. Browse the local artisan shops and art galleries, where you can find handcrafted jewelry, textiles, pottery, and other one-of-a-kind items. Take home a piece of Mexican culture and support local artists.", + "locationName": "Centro Histórico and surrounding areas", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-miguel-de-allende", + "ref": "shop-for-unique-souvenirs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_shop-for-unique-souvenirs.jpg" + }, + { + "name": "Take a Day Trip to Guanajuato", + "description": "Embark on a day trip to the nearby city of Guanajuato, another UNESCO World Heritage site. Explore the colorful colonial architecture, visit the historic silver mines, and wander through the charming alleyways. Don't miss the opportunity to visit the iconic Callejon del Beso (Alley of the Kiss) and learn about its romantic legend.", + "locationName": "Guanajuato City", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-miguel-de-allende", + "ref": "take-a-day-trip-to-guanajuato", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_take-a-day-trip-to-guanajuato.jpg" + }, + { + "name": "Wine Tour in the Valley", + "description": "Embark on a delightful journey through the burgeoning wine region surrounding San Miguel de Allende. Visit local vineyards, sample exquisite wines, and learn about the unique terroir and winemaking techniques of the region. Enjoy breathtaking vineyard vistas and indulge in a gourmet picnic lunch amidst the rolling hills.", + "locationName": "San Miguel de Allende Valley", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "san-miguel-de-allende", + "ref": "wine-tour-in-the-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_wine-tour-in-the-valley.jpg" + }, + { + "name": "Charreada Experience", + "description": "Immerse yourself in Mexican culture with a thrilling Charreada, a traditional rodeo-like event. Witness skilled Charros showcase their horsemanship, roping skills, and daring stunts. Enjoy lively music, vibrant costumes, and authentic Mexican food for a truly unforgettable experience.", + "locationName": "Lienzo Charro", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-miguel-de-allende", + "ref": "charreada-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_charreada-experience.jpg" + }, + { + "name": "Temazcal Ritual", + "description": "Experience a traditional Temazcal ceremony, a pre-Hispanic sweat lodge ritual used for purification and healing. Enter a dome-shaped structure and participate in a guided ceremony involving heated volcanic rocks, herbal steam, and chanting. Emerge feeling refreshed, rejuvenated, and connected to ancient traditions.", + "locationName": "Various locations throughout the city", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "san-miguel-de-allende", + "ref": "temazcal-ritual", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_temazcal-ritual.jpg" + }, + { + "name": "Stargazing in the Desert", + "description": "Escape the city lights and venture into the surrounding desert for a magical stargazing experience. Join a guided tour led by an astronomy expert who will unveil the wonders of the night sky. Marvel at constellations, planets, and distant galaxies, and learn about the celestial stories and myths.", + "locationName": "San Miguel de Allende Desert", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-miguel-de-allende", + "ref": "stargazing-in-the-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_stargazing-in-the-desert.jpg" + }, + { + "name": "Parroquia de San Miguel Arcángel Light Show", + "description": "As the sun sets, witness the iconic Parroquia de San Miguel Arcángel come alive with a mesmerizing light show. The church's neo-Gothic facade is illuminated with vibrant colors and intricate patterns, creating a breathtaking spectacle that reflects the city's artistic spirit.", + "locationName": "Parroquia de San Miguel Arcángel", + "duration": 1, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-miguel-de-allende", + "ref": "parroquia-de-san-miguel-arc-ngel-light-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-miguel-de-allende_parroquia-de-san-miguel-arc-ngel-light-show.jpg" + }, + { + "name": "Pintxos Hopping in the Old Town", + "description": "Embark on a delightful culinary adventure through the charming streets of San Sebastian's Old Town. Sample an array of delicious pintxos, small bites typically served on bread, at various bars and restaurants. Each establishment boasts its own unique specialties, from classic combinations to innovative creations. Savor the flavors, soak up the lively atmosphere, and discover why San Sebastian is a food lover's paradise.", + "locationName": "Old Town", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-sebastian", + "ref": "pintxos-hopping-in-the-old-town", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_pintxos-hopping-in-the-old-town.jpg" + }, + { + "name": "Beach Bliss at La Concha", + "description": "Unwind on the golden sands of La Concha, one of the most beautiful urban beaches in Europe. Bask in the sun, take a refreshing dip in the turquoise waters, or simply stroll along the picturesque promenade. Rent a paddleboard or kayak for a fun water adventure. In the evening, enjoy a breathtaking sunset over the bay.", + "locationName": "La Concha Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-sebastian", + "ref": "beach-bliss-at-la-concha", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_beach-bliss-at-la-concha.jpg" + }, + { + "name": "Hiking Mount Urgull for Panoramic Views", + "description": "Embark on a scenic hike up Mount Urgull, a historic hill overlooking the city. Ascend through lush greenery and discover remnants of ancient fortifications. Reach the summit and be rewarded with breathtaking panoramic views of San Sebastian, the bay, and the surrounding mountains. Visit the Mota Castle and the Sagrado Corazón statue for a dose of history and culture.", + "locationName": "Mount Urgull", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-sebastian", + "ref": "hiking-mount-urgull-for-panoramic-views", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_hiking-mount-urgull-for-panoramic-views.jpg" + }, + { + "name": "Michelin-Starred Culinary Experience", + "description": "Indulge in a world-class dining experience at one of San Sebastian's renowned Michelin-starred restaurants. Immerse yourself in the artistry of Basque cuisine as you savor exquisitely crafted dishes prepared with the finest local ingredients. Be captivated by the elegant ambiance, impeccable service, and innovative culinary creations that will tantalize your taste buds.", + "locationName": "Various Michelin-starred restaurants", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "san-sebastian", + "ref": "michelin-starred-culinary-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_michelin-starred-culinary-experience.jpg" + }, + { + "name": "Exploring the Charms of Santa Clara Island", + "description": "Take a boat trip to Santa Clara Island, a small island located in the middle of La Concha Bay. Relax on the island's secluded beach, swim in the crystal-clear waters, or hike to the top for panoramic views. Discover the island's lighthouse and small chapel, and enjoy a peaceful escape from the bustling city.", + "locationName": "Santa Clara Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-sebastian", + "ref": "exploring-the-charms-of-santa-clara-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_exploring-the-charms-of-santa-clara-island.jpg" + }, + { + "name": "Surfing at Zurriola Beach", + "description": "Catch some waves at Zurriola Beach, a haven for surfers of all levels. Rent a board and take a lesson, or simply watch the pros in action. The lively atmosphere and stunning coastal views make it a perfect spot to spend an active afternoon.", + "locationName": "Zurriola Beach", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-sebastian", + "ref": "surfing-at-zurriola-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_surfing-at-zurriola-beach.jpg" + }, + { + "name": "Kayaking or Stand Up Paddleboarding in La Concha Bay", + "description": "Enjoy a unique perspective of San Sebastian's coastline by kayaking or stand up paddleboarding in the calm waters of La Concha Bay. Glide past iconic landmarks like the Santa Clara Island and admire the elegant architecture of the city from the water. It's a relaxing and scenic way to experience the beauty of the bay.", + "locationName": "La Concha Bay", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-sebastian", + "ref": "kayaking-or-stand-up-paddleboarding-in-la-concha-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_kayaking-or-stand-up-paddleboarding-in-la-concha-bay.jpg" + }, + { + "name": "Exploring the Parte Vieja (Old Town)", + "description": "Get lost in the charming Parte Vieja, the historic heart of San Sebastian. Wander through narrow streets lined with traditional Basque buildings, discover hidden plazas, and pop into local shops and pintxos bars. Soak in the vibrant atmosphere and enjoy the architectural beauty of this historic district.", + "locationName": "Parte Vieja", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-sebastian", + "ref": "exploring-the-parte-vieja-old-town-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_exploring-the-parte-vieja-old-town-.jpg" + }, + { + "name": "Day Trip to the French Basque Country", + "description": "Embark on a day trip to the charming towns of the French Basque Country, such as Biarritz and Saint-Jean-de-Luz. Experience the unique culture and traditions of this region, indulge in delicious French cuisine, and explore the picturesque coastal landscapes. It's a perfect opportunity to discover the French side of the Basque Country.", + "locationName": "Biarritz and Saint-Jean-de-Luz", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "san-sebastian", + "ref": "day-trip-to-the-french-basque-country", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_day-trip-to-the-french-basque-country.jpg" + }, + { + "name": "Peine del Viento Sculptures and Paseo Nuevo", + "description": "Take a stroll along the Paseo Nuevo, a beautiful promenade offering breathtaking views of the Bay of Biscay. Admire the iconic Peine del Viento sculptures by Eduardo Chillida, a series of wind combs anchored to the rocks, and capture stunning photos of this unique art installation against the dramatic coastal backdrop.", + "locationName": "Paseo Nuevo", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-sebastian", + "ref": "peine-del-viento-sculptures-and-paseo-nuevo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_peine-del-viento-sculptures-and-paseo-nuevo.jpg" + }, + { + "name": "Sunset Sailing on the Bay of Biscay", + "description": "Embark on a magical sunset sail along the stunning Bay of Biscay. As the sun dips below the horizon, painting the sky with vibrant hues, admire the city's coastline from a unique perspective. Enjoy the gentle sea breeze and the tranquility of the open water, perhaps even spotting dolphins playing in the waves.", + "locationName": "Bay of Biscay", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-sebastian", + "ref": "sunset-sailing-on-the-bay-of-biscay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_sunset-sailing-on-the-bay-of-biscay.jpg" + }, + { + "name": "Cider House Experience in the Countryside", + "description": "Venture into the picturesque Basque countryside to discover the tradition of cider houses. Enjoy a rustic feast of chorizo, cod omelet, and steak, all accompanied by free-flowing cider poured directly from enormous barrels. Immerse yourself in the lively atmosphere and local culture, often accompanied by live music and singing.", + "locationName": "Basque Countryside", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-sebastian", + "ref": "cider-house-experience-in-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_cider-house-experience-in-the-countryside.jpg" + }, + { + "name": "Time Travel at the San Telmo Museum", + "description": "Delve into the rich history and culture of the Basque region at the San Telmo Museum. Explore fascinating exhibits showcasing archaeological artifacts, traditional costumes, and artwork, spanning centuries of Basque heritage. Gain a deeper understanding of the unique identity and traditions of this captivating region.", + "locationName": "San Telmo Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-sebastian", + "ref": "time-travel-at-the-san-telmo-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_time-travel-at-the-san-telmo-museum.jpg" + }, + { + "name": "Indulge in Sweet Treats at a Local Pastelería", + "description": "Satisfy your sweet tooth with a visit to one of San Sebastian's charming pastelerías. Delight in a variety of delectable pastries, cakes, and confections, all made with fresh, local ingredients. Savor the flavors of Basque gastronomy and experience the art of traditional baking.", + "locationName": "Various locations throughout the city", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-sebastian", + "ref": "indulge-in-sweet-treats-at-a-local-pasteler-a", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_indulge-in-sweet-treats-at-a-local-pasteler-a.jpg" + }, + { + "name": "Catch a Performance at the Victoria Eugenia Theatre", + "description": "Experience the magic of live performance at the historic Victoria Eugenia Theatre. Choose from a diverse program of plays, concerts, dance shows, and opera, showcasing local and international talent. Immerse yourself in the world of performing arts and enjoy a memorable evening in this architectural gem.", + "locationName": "Victoria Eugenia Theatre", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-sebastian", + "ref": "catch-a-performance-at-the-victoria-eugenia-theatre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_catch-a-performance-at-the-victoria-eugenia-theatre.jpg" + }, + { + "name": "Monte Igueldo Funicular Ride and Amusement Park", + "description": "Embark on a nostalgic journey on the historic Monte Igueldo funicular, offering breathtaking panoramic views of the city and coastline. At the top, enjoy the charming amusement park with vintage rides, games, and a delightful atmosphere perfect for families and those seeking a touch of whimsy.", + "locationName": "Monte Igueldo", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-sebastian", + "ref": "monte-igueldo-funicular-ride-and-amusement-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_monte-igueldo-funicular-ride-and-amusement-park.jpg" + }, + { + "name": "Comb of the Wind and Ondarreta Beach Stroll", + "description": "Take a leisurely walk along the picturesque Paseo Nuevo, marveling at the iconic Comb of the Wind sculptures by Eduardo Chillida. Continue to the serene Ondarreta Beach, perfect for sunbathing, swimming, or simply enjoying the tranquil atmosphere. Unwind with a drink or meal at one of the charming beachfront cafes.", + "locationName": "Paseo Nuevo and Ondarreta Beach", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-sebastian", + "ref": "comb-of-the-wind-and-ondarreta-beach-stroll", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_comb-of-the-wind-and-ondarreta-beach-stroll.jpg" + }, + { + "name": "Aquarium and Naval Museum Exploration", + "description": "Dive into the fascinating underwater world at the San Sebastian Aquarium, home to diverse marine species and interactive exhibits. Adjacent to the aquarium, delve into the rich maritime history of the region at the Naval Museum, exploring historical artifacts and model ships.", + "locationName": "San Sebastian Aquarium and Naval Museum", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "san-sebastian", + "ref": "aquarium-and-naval-museum-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_aquarium-and-naval-museum-exploration.jpg" + }, + { + "name": "La Bretxa Market and Culinary Delights", + "description": "Immerse yourself in the vibrant atmosphere of La Bretxa Market, a bustling hub of local produce, fresh seafood, and artisanal products. Sample Basque cheeses, cured meats, and other regional specialties. Enjoy a delightful lunch at a nearby restaurant, savoring the authentic flavors of San Sebastian.", + "locationName": "La Bretxa Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "san-sebastian", + "ref": "la-bretxa-market-and-culinary-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_la-bretxa-market-and-culinary-delights.jpg" + }, + { + "name": "Miramar Palace and Gardens", + "description": "Step back in time at the elegant Miramar Palace, a former royal summer residence boasting stunning architecture and picturesque gardens. Explore the palace grounds, enjoying panoramic views of the bay and surrounding landscapes. Pack a picnic or visit the nearby cafe for a delightful afternoon.", + "locationName": "Miramar Palace", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "san-sebastian", + "ref": "miramar-palace-and-gardens", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/san-sebastian_miramar-palace-and-gardens.jpg" + }, + { + "name": "Sunset Catamaran Cruise with Dinner and Drinks", + "description": "Embark on a romantic evening cruise aboard a catamaran, sailing along the caldera cliffs as the sun dips below the horizon. Indulge in a delicious dinner featuring local Greek specialties while enjoying breathtaking views of the Aegean Sea. The cruise also includes stops for swimming and snorkeling in secluded coves, allowing you to experience the beauty of the island from a unique perspective. As the sky transforms into a canvas of vibrant colors, raise a glass of Santorini's renowned wine and create unforgettable memories with your loved one.", + "locationName": "Ammoudi Bay", + "duration": 5, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "santorini", + "ref": "sunset-catamaran-cruise-with-dinner-and-drinks", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_sunset-catamaran-cruise-with-dinner-and-drinks.jpg" + }, + { + "name": "Explore the Ancient City of Akrotiri", + "description": "Step back in time and discover the fascinating ruins of Akrotiri, a Minoan Bronze Age settlement buried by volcanic ash thousands of years ago. Explore the remarkably preserved streets, houses, and frescoes that offer a glimpse into the daily life of this ancient civilization. Learn about the advanced culture and artistry of the Minoans and marvel at the engineering feats that allowed them to thrive on this volcanic island.", + "locationName": "Akrotiri Archaeological Site", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "santorini", + "ref": "explore-the-ancient-city-of-akrotiri", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_explore-the-ancient-city-of-akrotiri.jpg" + }, + { + "name": "Hike from Fira to Oia", + "description": "Embark on a scenic hike along the caldera rim, connecting the villages of Fira and Oia. The trail offers breathtaking panoramic views of the volcanic caldera, the Aegean Sea, and the iconic white-washed villages that cling to the cliffs. Along the way, you'll encounter charming churches, traditional houses, and hidden viewpoints. This moderate hike is a perfect way to immerse yourself in the beauty of Santorini's landscape and capture stunning photographs.", + "locationName": "Fira to Oia Trail", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "santorini", + "ref": "hike-from-fira-to-oia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_hike-from-fira-to-oia.jpg" + }, + { + "name": "Relax on the Black Sand Beaches", + "description": "Escape to the unique black sand beaches of Santorini, formed by volcanic eruptions centuries ago. Perissa and Kamari beaches offer a vibrant atmosphere with beach clubs, water sports, and beachfront restaurants. For a more secluded experience, head to the Red Beach, known for its striking red cliffs and crystal-clear waters. Soak up the sun, swim in the Aegean Sea, and enjoy the contrast of the dark sand against the turquoise waters.", + "locationName": "Perissa, Kamari, or Red Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "santorini", + "ref": "relax-on-the-black-sand-beaches", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_relax-on-the-black-sand-beaches.jpg" + }, + { + "name": "Santorini Wine Tour and Tasting", + "description": "Embark on a journey through Santorini's renowned winemaking tradition with a guided tour of local wineries. Discover the unique grape varieties that thrive in the island's volcanic soil and learn about the traditional winemaking methods. Sample a selection of Santorini's distinctive wines, including the crisp Assyrtiko, the sweet Vinsanto, and the full-bodied Nykteri. Enjoy the idyllic vineyard settings and gain insights into the island's rich viticulture heritage.", + "locationName": "Various wineries in Santorini", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "santorini", + "ref": "santorini-wine-tour-and-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_santorini-wine-tour-and-tasting.jpg" + }, + { + "name": "Scuba Dive the Aegean Sea", + "description": "Delve into the crystal-clear waters of the Aegean Sea and discover a mesmerizing underwater world. Explore vibrant coral reefs, encounter fascinating marine life like octopuses and sea turtles, and swim through ancient shipwrecks. Several dive centers on the island cater to all levels, from beginners to experienced divers.", + "locationName": "Various dive sites around Santorini", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "santorini", + "ref": "scuba-dive-the-aegean-sea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_scuba-dive-the-aegean-sea.jpg" + }, + { + "name": "Kayak to the Sea Caves of the Caldera", + "description": "Embark on a sea kayaking adventure along the dramatic caldera cliffs. Paddle through hidden sea caves, marvel at the volcanic rock formations, and enjoy breathtaking views of the Aegean Sea and the surrounding islands. This is a unique way to experience the beauty of Santorini from a different perspective.", + "locationName": "Caldera cliffs", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "santorini", + "ref": "kayak-to-the-sea-caves-of-the-caldera", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_kayak-to-the-sea-caves-of-the-caldera.jpg" + }, + { + "name": "Indulge in a Traditional Greek Cooking Class", + "description": "Immerse yourself in Greek culture by learning the secrets of traditional cuisine. Join a cooking class and master the art of preparing classic dishes like moussaka, souvlaki, and fresh Greek salad. Enjoy the fruits of your labor with a delicious meal accompanied by local wine.", + "locationName": "Various locations in Santorini", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "santorini", + "ref": "indulge-in-a-traditional-greek-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_indulge-in-a-traditional-greek-cooking-class.jpg" + }, + { + "name": "Explore the Picturesque Village of Oia at Night", + "description": "As the sun sets, wander through the charming village of Oia and witness its magical transformation. The whitewashed houses and blue-domed churches are bathed in a warm glow, creating a romantic and enchanting atmosphere. Enjoy dinner at a cliffside restaurant with panoramic views or simply stroll through the narrow streets and soak up the ambiance.", + "locationName": "Oia", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "santorini", + "ref": "explore-the-picturesque-village-of-oia-at-night", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_explore-the-picturesque-village-of-oia-at-night.jpg" + }, + { + "name": "Hike to the Profitis Ilias Monastery", + "description": "Embark on a rewarding hike to the highest point on Santorini, where the Profitis Ilias Monastery stands proudly. Enjoy panoramic views of the entire island, the caldera, and the Aegean Sea. The monastery itself offers a peaceful atmosphere and a glimpse into the island's religious heritage.", + "locationName": "Mount Profitis Ilias", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "santorini", + "ref": "hike-to-the-profitis-ilias-monastery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_hike-to-the-profitis-ilias-monastery.jpg" + }, + { + "name": "Horseback Riding through Volcanic Landscapes", + "description": "Embark on a unique adventure, exploring the captivating volcanic landscapes of Santorini on horseback. Traverse rugged trails, witness breathtaking caldera views, and discover hidden corners of the island inaccessible by other means. This activity is perfect for nature enthusiasts and adventure seekers looking for an unforgettable experience.", + "locationName": "Megalochori or Akrotiri", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "santorini", + "ref": "horseback-riding-through-volcanic-landscapes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_horseback-riding-through-volcanic-landscapes.jpg" + }, + { + "name": "Sunset Sailing with a Local Skipper", + "description": "Set sail on a private or small-group sailing excursion as the sun begins its descent, painting the sky with vibrant hues. Your local skipper will share insights about the island and its hidden gems while you glide across the Aegean Sea. Enjoy swimming, snorkeling, and a delightful meal on board, creating an unforgettable evening.", + "locationName": "Amoudi Bay or Vlychada Marina", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "santorini", + "ref": "sunset-sailing-with-a-local-skipper", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_sunset-sailing-with-a-local-skipper.jpg" + }, + { + "name": "Discover the Lost Atlantis Experience", + "description": "Embark on a captivating journey to the Lost Atlantis Experience Museum. Explore interactive exhibits, delve into the myth of Atlantis, and learn about the Minoan civilization that once thrived on Santorini. This immersive experience offers a unique blend of history, mythology, and entertainment for all ages.", + "locationName": "Megalochori", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "santorini", + "ref": "discover-the-lost-atlantis-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_discover-the-lost-atlantis-experience.jpg" + }, + { + "name": "Indulge in a Luxurious Spa Day", + "description": "Escape the hustle and bustle of everyday life and treat yourself to a rejuvenating spa experience. Numerous luxury spas on Santorini offer a variety of treatments inspired by local traditions and ingredients, such as volcanic mud masks and olive oil massages. Unwind, de-stress, and emerge feeling refreshed and revitalized.", + "locationName": "Various locations in Oia, Imerovigli, or Fira", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "santorini", + "ref": "indulge-in-a-luxurious-spa-day", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_indulge-in-a-luxurious-spa-day.jpg" + }, + { + "name": "Explore the Island's Charming Villages on an ATV Adventure", + "description": "Rent an ATV and embark on a thrilling adventure, exploring Santorini's charming villages and hidden corners at your own pace. Discover traditional architecture, local shops, and stunning viewpoints, venturing off the beaten path for a unique perspective of the island's beauty.", + "locationName": "Fira or Perissa", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "santorini", + "ref": "explore-the-island-s-charming-villages-on-an-atv-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_explore-the-island-s-charming-villages-on-an-atv-adventure.jpg" + }, + { + "name": "Visit the Museum of Prehistoric Thera", + "description": "Delve into Santorini's rich history at the Museum of Prehistoric Thera. Explore fascinating artifacts from the Minoan civilization, including frescoes, pottery, and tools, and gain insights into the island's ancient past. This indoor activity is perfect for a rainy day or a break from the sun, offering a captivating journey through time.", + "locationName": "Fira", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "santorini", + "ref": "visit-the-museum-of-prehistoric-thera", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_visit-the-museum-of-prehistoric-thera.jpg" + }, + { + "name": "Take a Cooking Class with a Local Family", + "description": "Immerse yourself in Greek culture and culinary traditions with a cooking class hosted by a local family. Learn the secrets of preparing authentic dishes like moussaka, souvlaki, and baklava, using fresh, local ingredients. Enjoy the fruits of your labor with a shared meal, creating lasting memories and a deeper connection with the island's people.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "santorini", + "ref": "take-a-cooking-class-with-a-local-family", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_take-a-cooking-class-with-a-local-family.jpg" + }, + { + "name": "Go Cliff Jumping in Amoudi Bay", + "description": "For thrill-seekers, cliff jumping in Amoudi Bay is an exhilarating experience. Leap from rocky cliffs into the crystal-clear Aegean Sea, surrounded by stunning views of the caldera. This activity is best suited for adventurous travelers and strong swimmers, offering an adrenaline rush and unforgettable memories.", + "locationName": "Amoudi Bay", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 1, + "destinationRef": "santorini", + "ref": "go-cliff-jumping-in-amoudi-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_go-cliff-jumping-in-amoudi-bay.jpg" + }, + { + "name": "Shop for Unique Souvenirs in Oia", + "description": "Wander through the charming streets of Oia and discover a treasure trove of unique souvenirs. From handcrafted jewelry and ceramics to local artwork and textiles, find the perfect memento to remember your trip. Enjoy the vibrant atmosphere, browse through boutique shops, and support local artisans.", + "locationName": "Oia", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "santorini", + "ref": "shop-for-unique-souvenirs-in-oia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_shop-for-unique-souvenirs-in-oia.jpg" + }, + { + "name": "Catch a Movie Under the Stars at the Open Air Cinema Kamari", + "description": "Experience a magical evening at the Open Air Cinema Kamari. Relax under the starry sky and enjoy a classic or contemporary film in a unique outdoor setting. This is a perfect way to unwind after a day of exploring, offering a romantic and memorable experience.", + "locationName": "Kamari", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "santorini", + "ref": "catch-a-movie-under-the-stars-at-the-open-air-cinema-kamari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/santorini_catch-a-movie-under-the-stars-at-the-open-air-cinema-kamari.jpg" + }, + { + "name": "Hike the Selvaggio Blu", + "description": "Embark on a challenging multi-day trek along the Selvaggio Blu, renowned as one of Europe's most demanding hiking trails. Traverse dramatic cliffs, hidden coves, and dense forests, while enjoying breathtaking coastal views.", + "locationName": "Eastern Coast of Sardinia", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "sardinia", + "ref": "hike-the-selvaggio-blu", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_hike-the-selvaggio-blu.jpg" + }, + { + "name": "Explore the Ruins of Nora", + "description": "Step back in time at the ancient Roman city of Nora. Wander through the remnants of temples, baths, and houses, and imagine life during the Roman Empire.", + "locationName": "Nora", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sardinia", + "ref": "explore-the-ruins-of-nora", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_explore-the-ruins-of-nora.jpg" + }, + { + "name": "Relax on Cala Goloritzé Beach", + "description": "Unwind on the pristine sands of Cala Goloritzé, a secluded beach accessible only by boat or a challenging hike. Swim in crystal-clear turquoise waters, soak up the Mediterranean sun, and marvel at the towering limestone cliffs.", + "locationName": "Cala Goloritzé", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "sardinia", + "ref": "relax-on-cala-goloritz-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_relax-on-cala-goloritz-beach.jpg" + }, + { + "name": "Discover the Neptune's Grotto", + "description": "Embark on a boat tour to the Neptune's Grotto, a breathtaking cave system filled with stalactites, stalagmites, and an underground saltwater lake. Explore the illuminated chambers and marvel at the natural wonders.", + "locationName": "Alghero", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "sardinia", + "ref": "discover-the-neptune-s-grotto", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_discover-the-neptune-s-grotto.jpg" + }, + { + "name": "Indulge in Sardinian Cuisine", + "description": "Savor the flavors of Sardinia's unique cuisine. Sample local specialties such as pane carasau (crispy flatbread), fregola (pasta with clams), and porceddu (roast suckling pig). Pair your meal with a glass of Cannonau, a robust red wine.", + "locationName": "Various Restaurants", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "sardinia", + "ref": "indulge-in-sardinian-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_indulge-in-sardinian-cuisine.jpg" + }, + { + "name": "Go Caving in Is Zuddas Caves", + "description": "Embark on an underground adventure by exploring the Is Zuddas Caves, renowned for their impressive stalactites, stalagmites, and unique aragonite formations. Take a guided tour to delve into the depths of these geological wonders and learn about their fascinating history and formation.", + "locationName": "Is Zuddas Caves", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "sardinia", + "ref": "go-caving-in-is-zuddas-caves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_go-caving-in-is-zuddas-caves.jpg" + }, + { + "name": "Sail the Archipelago of La Maddalena", + "description": "Set sail on a boat trip to the Archipelago of La Maddalena, a collection of stunning islands and islets boasting pristine beaches, crystal-clear waters, and secluded coves. Enjoy swimming, snorkeling, and sunbathing in this idyllic paradise.", + "locationName": "Archipelago of La Maddalena", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "sardinia", + "ref": "sail-the-archipelago-of-la-maddalena", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_sail-the-archipelago-of-la-maddalena.jpg" + }, + { + "name": "Visit the Nuraghe Su Nuraxi", + "description": "Step back in time at the Nuraghe Su Nuraxi, a UNESCO World Heritage Site and one of the most impressive examples of Nuragic civilization. Explore the complex of stone towers, dating back to the Bronze Age, and discover the island's ancient history and culture.", + "locationName": "Nuraghe Su Nuraxi", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sardinia", + "ref": "visit-the-nuraghe-su-nuraxi", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_visit-the-nuraghe-su-nuraxi.jpg" + }, + { + "name": "Windsurf or Kitesurf on the Coast", + "description": "Embrace the wind and waves by trying windsurfing or kitesurfing along Sardinia's coast. With its consistent winds and beautiful beaches, the island offers ideal conditions for these exhilarating water sports. Take a lesson or rent equipment to experience the thrill of gliding across the water.", + "locationName": "Various beaches", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "sardinia", + "ref": "windsurf-or-kitesurf-on-the-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_windsurf-or-kitesurf-on-the-coast.jpg" + }, + { + "name": "Experience the Vibrant Nightlife of Cagliari", + "description": "As the sun sets, immerse yourself in the lively nightlife of Cagliari, the island's capital city. Explore the charming streets and squares, lined with bars, clubs, and restaurants. Enjoy live music, dancing, and socializing with locals and fellow travelers.", + "locationName": "Cagliari", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "sardinia", + "ref": "experience-the-vibrant-nightlife-of-cagliari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_experience-the-vibrant-nightlife-of-cagliari.jpg" + }, + { + "name": "Horseback Riding in Giara di Gesturi", + "description": "Embark on a unique horseback riding adventure through the Giara di Gesturi, a plateau known for its wild horses, stunning landscapes, and cork oak forests. This experience is perfect for nature lovers and adventure seekers, offering breathtaking views and a chance to connect with the island's wild side.", + "locationName": "Giara di Gesturi", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "sardinia", + "ref": "horseback-riding-in-giara-di-gesturi", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_horseback-riding-in-giara-di-gesturi.jpg" + }, + { + "name": "Delve into History at the National Archaeological Museum of Cagliari", + "description": "Step back in time at the National Archaeological Museum of Cagliari, home to a vast collection of artifacts from Sardinia's rich history. Explore exhibits showcasing prehistoric finds, Roman treasures, and Byzantine art, gaining insights into the island's fascinating past.", + "locationName": "Cagliari", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "sardinia", + "ref": "delve-into-history-at-the-national-archaeological-museum-of-cagliari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_delve-into-history-at-the-national-archaeological-museum-of-cagliari.jpg" + }, + { + "name": "Kayaking in the Gulf of Orosei", + "description": "Paddle through the crystal-clear waters of the Gulf of Orosei, exploring hidden coves, dramatic cliffs, and secluded beaches. Kayak along the coastline, discovering natural wonders like sea caves, rock formations, and diverse marine life. This activity is perfect for adventure seekers and nature enthusiasts.", + "locationName": "Gulf of Orosei", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "sardinia", + "ref": "kayaking-in-the-gulf-of-orosei", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_kayaking-in-the-gulf-of-orosei.jpg" + }, + { + "name": "Indulge in a Wine Tasting Tour", + "description": "Embark on a sensory journey through Sardinia's renowned wine regions. Visit local vineyards, learn about traditional winemaking techniques, and savor a variety of exquisite wines, from robust reds to crisp whites. This experience is perfect for wine enthusiasts and those seeking a taste of the island's cultural heritage.", + "locationName": "Various vineyards throughout Sardinia", + "duration": 5, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "sardinia", + "ref": "indulge-in-a-wine-tasting-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_indulge-in-a-wine-tasting-tour.jpg" + }, + { + "name": "Go Birdwatching in the Molentargius Saline Regional Park", + "description": "Discover a haven for birdwatchers at the Molentargius Saline Regional Park, a wetland area teeming with diverse avian species. Observe flamingos, herons, egrets, and other migratory birds in their natural habitat. This peaceful and educational experience is perfect for nature lovers and families.", + "locationName": "Molentargius Saline Regional Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "sardinia", + "ref": "go-birdwatching-in-the-molentargius-saline-regional-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_go-birdwatching-in-the-molentargius-saline-regional-park.jpg" + }, + { + "name": "Scuba Dive in the Marine Protected Area of Tavolara", + "description": "Embark on an underwater adventure in the crystal-clear waters of the Tavolara Marine Protected Area. Discover a vibrant world of marine life, including colorful fish, octopuses, and even dolphins. Explore underwater caves and shipwrecks, and witness the beauty of Sardinia's marine ecosystem.", + "locationName": "Tavolara Island", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "sardinia", + "ref": "scuba-dive-in-the-marine-protected-area-of-tavolara", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_scuba-dive-in-the-marine-protected-area-of-tavolara.jpg" + }, + { + "name": "Take a Cooking Class and Learn to Make Traditional Sardinian Dishes", + "description": "Immerse yourself in Sardinian culture by participating in a hands-on cooking class. Learn the secrets of preparing authentic dishes like culurgiones (stuffed pasta), porceddu (roast suckling pig), and seadas (fried cheese pastries). Enjoy the fruits of your labor with a delicious meal accompanied by local wine.", + "locationName": "Various locations throughout Sardinia", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "sardinia", + "ref": "take-a-cooking-class-and-learn-to-make-traditional-sardinian-dishes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_take-a-cooking-class-and-learn-to-make-traditional-sardinian-dishes.jpg" + }, + { + "name": "Go Stargazing in the Supramonte Mountains", + "description": "Escape the city lights and venture into the Supramonte Mountains for an unforgettable stargazing experience. The remote location and clear skies offer breathtaking views of the Milky Way and constellations. Join a guided tour or find a secluded spot to marvel at the celestial wonders above.", + "locationName": "Supramonte Mountains", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "sardinia", + "ref": "go-stargazing-in-the-supramonte-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_go-stargazing-in-the-supramonte-mountains.jpg" + }, + { + "name": "Visit the Medieval Town of Castelsardo", + "description": "Step back in time with a visit to Castelsardo, a charming medieval town perched on a hilltop overlooking the sea. Explore the narrow cobblestone streets, admire the colorful houses, and visit the Castello dei Doria, a 12th-century castle with stunning views. Discover local artisan shops and enjoy fresh seafood at a waterfront restaurant.", + "locationName": "Castelsardo", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sardinia", + "ref": "visit-the-medieval-town-of-castelsardo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_visit-the-medieval-town-of-castelsardo.jpg" + }, + { + "name": "Take a Boat Trip to the Island of Asinara", + "description": "Embark on a boat trip to the Island of Asinara, a former prison island turned national park. Discover the island's unique history, observe the diverse wildlife, including wild albino donkeys, and relax on secluded beaches. Enjoy swimming, snorkeling, or simply soaking up the sun in this pristine natural environment.", + "locationName": "Asinara Island", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "sardinia", + "ref": "take-a-boat-trip-to-the-island-of-asinara", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sardinia_take-a-boat-trip-to-the-island-of-asinara.jpg" + }, + { + "name": "Hike the West Highland Way", + "description": "Embark on an epic journey through the Scottish Highlands, trekking the famous West Highland Way. This long-distance trail winds through breathtaking landscapes, from the shores of Loch Lomond to the foot of Ben Nevis, the UK's highest peak. Immerse yourself in the rugged beauty of the mountains, encounter charming villages, and witness the untamed wilderness of Scotland.", + "locationName": "Scottish Highlands", + "duration": 152, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "scotland", + "ref": "hike-the-west-highland-way", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_hike-the-west-highland-way.jpg" + }, + { + "name": "Explore Edinburgh Castle", + "description": "Step back in time with a visit to Edinburgh Castle, perched atop an extinct volcano overlooking the city. Delve into Scotland's rich history as you explore the castle's ancient walls, grand halls, and the Crown Jewels of Scotland. Enjoy panoramic views of Edinburgh and learn about the castle's fascinating role in Scottish history.", + "locationName": "Edinburgh", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scotland", + "ref": "explore-edinburgh-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_explore-edinburgh-castle.jpg" + }, + { + "name": "Discover the Isle of Skye", + "description": "Escape to the enchanting Isle of Skye, renowned for its dramatic scenery, charming villages, and mythical legends. Hike to the Old Man of Storr, a towering rock formation, or visit the Fairy Pools, a series of crystal-clear waterfalls and pools. Explore the island's rich history and folklore, and soak in the magical atmosphere of this iconic Scottish destination.", + "locationName": "Isle of Skye", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "scotland", + "ref": "discover-the-isle-of-skye", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_discover-the-isle-of-skye.jpg" + }, + { + "name": "Visit a Scotch Whisky Distillery", + "description": "Embark on a sensory journey at a Scotch whisky distillery, where you can learn about the intricate process of making this iconic spirit. Tour the distillery, witness the craftsmanship behind each bottle, and indulge in a tasting session to savor the distinct flavors of Scotch whisky. Discover the history and heritage of this beloved Scottish tradition.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "scotland", + "ref": "visit-a-scotch-whisky-distillery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_visit-a-scotch-whisky-distillery.jpg" + }, + { + "name": "Attend the Royal Edinburgh Military Tattoo", + "description": "Experience a spectacular display of music, dance, and military pageantry at the Royal Edinburgh Military Tattoo. Held annually on the esplanade of Edinburgh Castle, this world-renowned event features performances by military bands, cultural groups, and talented artists from around the globe. Be captivated by the vibrant colors, stirring music, and unforgettable atmosphere of this iconic Scottish tradition.", + "locationName": "Edinburgh Castle", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 4, + "destinationRef": "scotland", + "ref": "attend-the-royal-edinburgh-military-tattoo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_attend-the-royal-edinburgh-military-tattoo.jpg" + }, + { + "name": "Go Nessie Hunting at Loch Ness", + "description": "Embark on a thrilling boat tour of the legendary Loch Ness, seeking a glimpse of the elusive Loch Ness Monster. Learn about the history and myths surrounding Nessie while enjoying the stunning scenery of the Scottish Highlands. Keep your eyes peeled for a mysterious ripple or a long, serpentine neck emerging from the depths!", + "locationName": "Loch Ness", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scotland", + "ref": "go-nessie-hunting-at-loch-ness", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_go-nessie-hunting-at-loch-ness.jpg" + }, + { + "name": "Take a Scenic Drive on the North Coast 500", + "description": "Embark on an epic road trip along the North Coast 500, a 500-mile route that winds through the breathtaking landscapes of the Scottish Highlands. Discover hidden beaches, dramatic cliffs, charming villages, and ancient castles. Capture stunning photos at every turn and immerse yourself in the wild beauty of the north.", + "locationName": "North Coast 500", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "scotland", + "ref": "take-a-scenic-drive-on-the-north-coast-500", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_take-a-scenic-drive-on-the-north-coast-500.jpg" + }, + { + "name": "Visit the Magical Isle of Staffa", + "description": "Take a boat tour to the Isle of Staffa, a small, uninhabited island known for its unique geological formations. Explore Fingal's Cave, a sea cave with towering hexagonal basalt columns, and marvel at the island's diverse birdlife, including puffins and guillemots. This natural wonder is sure to leave you spellbound.", + "locationName": "Isle of Staffa", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "scotland", + "ref": "visit-the-magical-isle-of-staffa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_visit-the-magical-isle-of-staffa.jpg" + }, + { + "name": "Experience the Edinburgh Fringe Festival", + "description": "Immerse yourself in the world's largest arts festival, the Edinburgh Fringe Festival. With thousands of shows spanning theater, comedy, music, dance, and more, there's something for everyone. Enjoy the vibrant atmosphere, discover hidden gems, and be entertained by talented performers from around the globe.", + "locationName": "Edinburgh", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "scotland", + "ref": "experience-the-edinburgh-fringe-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_experience-the-edinburgh-fringe-festival.jpg" + }, + { + "name": "Go on a Wildlife Safari in the Cairngorms National Park", + "description": "Embark on a wildlife safari adventure in the Cairngorms National Park, home to a diverse range of animals. Look out for red deer, golden eagles, ospreys, red squirrels, and more as you explore the stunning mountain landscapes. Learn about the park's conservation efforts and experience the thrill of encountering these magnificent creatures in their natural habitat.", + "locationName": "Cairngorms National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "scotland", + "ref": "go-on-a-wildlife-safari-in-the-cairngorms-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_go-on-a-wildlife-safari-in-the-cairngorms-national-park.jpg" + }, + { + "name": "Kayaking on Loch Lomond", + "description": "Embark on a serene kayaking adventure on the glistening waters of Loch Lomond, the largest freshwater lake in Great Britain. Paddle amidst stunning scenery, with majestic mountains as your backdrop and charming islands dotting the horizon. Keep an eye out for diverse wildlife, including ospreys soaring above and playful otters frolicking along the shores. Whether you're a seasoned paddler or a first-timer, kayaking on Loch Lomond offers a tranquil escape into nature's embrace.", + "locationName": "Loch Lomond", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scotland", + "ref": "kayaking-on-loch-lomond", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_kayaking-on-loch-lomond.jpg" + }, + { + "name": "Step Back in Time at the Highland Folk Museum", + "description": "Immerse yourself in Scotland's rich history at the Highland Folk Museum, an open-air living history museum nestled amidst the scenic Cairngorms National Park. Explore authentic period buildings, from thatched cottages to traditional farmsteads, and encounter costumed interpreters who bring the past to life. Learn about ancient crafts, farming techniques, and the daily lives of Highland people through the centuries. It's an engaging and educational experience for all ages.", + "locationName": "Newtonmore", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scotland", + "ref": "step-back-in-time-at-the-highland-folk-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_step-back-in-time-at-the-highland-folk-museum.jpg" + }, + { + "name": "Indulge in a Culinary Journey on a Food Tour", + "description": "Embark on a delectable food tour through Scotland's vibrant culinary scene. Sample fresh seafood delicacies in charming coastal towns, savor the rich flavors of traditional haggis, and indulge in artisanal cheeses and locally-sourced produce. Discover the secrets behind Scotland's renowned whisky production with a distillery visit and tasting. From Michelin-starred restaurants to cozy pubs, a food tour promises a tantalizing exploration of Scottish flavors.", + "locationName": "Various cities and regions", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "scotland", + "ref": "indulge-in-a-culinary-journey-on-a-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_indulge-in-a-culinary-journey-on-a-food-tour.jpg" + }, + { + "name": "Go Mountain Biking in the Scottish Highlands", + "description": "Experience the thrill of mountain biking amidst the breathtaking landscapes of the Scottish Highlands. Explore challenging trails with steep climbs and exhilarating descents, or opt for leisurely paths through rolling hills and alongside sparkling lochs. With diverse terrain and stunning scenery at every turn, mountain biking in the Highlands is an adventure that will get your adrenaline pumping and leave you with unforgettable memories.", + "locationName": "Scottish Highlands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "scotland", + "ref": "go-mountain-biking-in-the-scottish-highlands", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_go-mountain-biking-in-the-scottish-highlands.jpg" + }, + { + "name": "Take a Boat Trip to Spot Marine Wildlife", + "description": "Embark on a thrilling boat trip along Scotland's rugged coastline to encounter an array of fascinating marine wildlife. Keep your eyes peeled for playful dolphins leaping through the waves, majestic whales breaching the surface, and adorable puffins perched on rocky cliffs. Experienced guides will share their knowledge about the local ecosystem and provide insights into the behaviors of these incredible creatures. It's a memorable experience for nature enthusiasts of all ages.", + "locationName": "Various coastal locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scotland", + "ref": "take-a-boat-trip-to-spot-marine-wildlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_take-a-boat-trip-to-spot-marine-wildlife.jpg" + }, + { + "name": "Unwind at a Traditional Scottish Pub", + "description": "Experience the warm and inviting atmosphere of a traditional Scottish pub. Enjoy live folk music, sample local craft beers and whiskies, and mingle with friendly locals. Immerse yourself in the authentic Scottish culture and create unforgettable memories.", + "locationName": "Various pubs across Scotland", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "scotland", + "ref": "unwind-at-a-traditional-scottish-pub", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_unwind-at-a-traditional-scottish-pub.jpg" + }, + { + "name": "Go Golfing on a World-Renowned Course", + "description": "Scotland is the birthplace of golf, and the country boasts some of the most iconic golf courses in the world. Tee off amidst stunning landscapes and challenge yourself on historic greens. Whether you're a seasoned golfer or a beginner, a round of golf in Scotland is an unforgettable experience.", + "locationName": "St Andrews Old Course, Royal Dornoch Golf Club, Muirfield Village Golf Club", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "scotland", + "ref": "go-golfing-on-a-world-renowned-course", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_go-golfing-on-a-world-renowned-course.jpg" + }, + { + "name": "Explore the Ruins of Urquhart Castle", + "description": "Journey back in time as you explore the ruins of Urquhart Castle, perched on the banks of Loch Ness. Discover its rich history, from medieval times to its role in the Jacobite risings. Take in the breathtaking views of the loch and surrounding mountains, and imagine the lives of those who once called this castle home.", + "locationName": "Urquhart Castle, Loch Ness", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "scotland", + "ref": "explore-the-ruins-of-urquhart-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_explore-the-ruins-of-urquhart-castle.jpg" + }, + { + "name": "Take a Train Journey on the Jacobite Steam Train", + "description": "Embark on a magical journey through the Scottish Highlands aboard the Jacobite Steam Train, also known as the \"Hogwarts Express\" from the Harry Potter films. Traverse picturesque landscapes, cross the Glenfinnan Viaduct, and enjoy the nostalgic charm of steam travel. This scenic train ride is a must-do for Harry Potter fans and nature enthusiasts alike.", + "locationName": "Fort William to Mallaig", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "scotland", + "ref": "take-a-train-journey-on-the-jacobite-steam-train", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_take-a-train-journey-on-the-jacobite-steam-train.jpg" + }, + { + "name": "Visit the Kelvingrove Art Gallery and Museum", + "description": "Immerse yourself in art and culture at the Kelvingrove Art Gallery and Museum in Glasgow. Explore a diverse collection of exhibits, including natural history, arms and armor, and European and Scottish art. Admire masterpieces by renowned artists and discover fascinating artifacts from around the world.", + "locationName": "Kelvingrove Art Gallery and Museum, Glasgow", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scotland", + "ref": "visit-the-kelvingrove-art-gallery-and-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scotland_visit-the-kelvingrove-art-gallery-and-museum.jpg" + }, + { + "name": "Hike Ben Nevis", + "description": "Embark on a challenging yet rewarding hike up Ben Nevis, the highest peak in the British Isles. Enjoy breathtaking panoramic views of the surrounding mountains and valleys, and feel a sense of accomplishment as you reach the summit.", + "locationName": "Ben Nevis", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "scottish-highlands", + "ref": "hike-ben-nevis", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_hike-ben-nevis.jpg" + }, + { + "name": "Explore Eilean Donan Castle", + "description": "Step back in time at Eilean Donan Castle, a picturesque fortress situated on a rocky islet at the meeting point of three sea lochs. Discover the castle's rich history, admire its stunning architecture, and take in the scenic surroundings.", + "locationName": "Eilean Donan Castle", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scottish-highlands", + "ref": "explore-eilean-donan-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_explore-eilean-donan-castle.jpg" + }, + { + "name": "Visit a Whisky Distillery", + "description": "Immerse yourself in the world of Scotch whisky with a tour and tasting at a local distillery. Learn about the whisky-making process, from the selection of grains to the aging in oak casks, and sample a variety of single malts.", + "locationName": "Various distilleries", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "scottish-highlands", + "ref": "visit-a-whisky-distillery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_visit-a-whisky-distillery.jpg" + }, + { + "name": "Take a Scenic Drive on the North Coast 500", + "description": "Embark on a road trip along the North Coast 500, a breathtaking coastal route that winds its way through the Scottish Highlands. Discover hidden beaches, charming villages, and dramatic cliffs, and stop to admire the stunning scenery along the way.", + "locationName": "North Coast 500", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scottish-highlands", + "ref": "take-a-scenic-drive-on-the-north-coast-500", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_take-a-scenic-drive-on-the-north-coast-500.jpg" + }, + { + "name": "Go Wildlife Watching in the Cairngorms National Park", + "description": "Explore the Cairngorms National Park, home to a diverse array of wildlife. Keep an eye out for red deer, golden eagles, ospreys, and other native species as you hike through the park's forests and mountains.", + "locationName": "Cairngorms National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "scottish-highlands", + "ref": "go-wildlife-watching-in-the-cairngorms-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_go-wildlife-watching-in-the-cairngorms-national-park.jpg" + }, + { + "name": "Kayaking on Loch Ness", + "description": "Embark on a serene kayaking adventure on the legendary Loch Ness. Paddle through the calm waters, surrounded by breathtaking mountain scenery, and keep an eye out for the mythical Loch Ness Monster. This is a fantastic way to experience the natural beauty of the Highlands and create unforgettable memories.", + "locationName": "Loch Ness", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scottish-highlands", + "ref": "kayaking-on-loch-ness", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_kayaking-on-loch-ness.jpg" + }, + { + "name": "Stargazing in Galloway Forest Park", + "description": "Escape the city lights and immerse yourself in the wonders of the night sky at Galloway Forest Park, a designated Dark Sky Park. With minimal light pollution, the park offers exceptional stargazing opportunities. Join a guided tour or simply lie back and marvel at the constellations, planets, and the Milky Way.", + "locationName": "Galloway Forest Park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "scottish-highlands", + "ref": "stargazing-in-galloway-forest-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_stargazing-in-galloway-forest-park.jpg" + }, + { + "name": "Mountain Biking in the Cairngorms", + "description": "Experience the thrill of mountain biking amidst the stunning landscapes of the Cairngorms National Park. With a variety of trails catering to different skill levels, you can choose your own adventure. Explore rugged mountain paths, dense forests, and picturesque valleys while enjoying the fresh air and breathtaking views.", + "locationName": "Cairngorms National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "scottish-highlands", + "ref": "mountain-biking-in-the-cairngorms", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_mountain-biking-in-the-cairngorms.jpg" + }, + { + "name": "Explore the Isle of Skye", + "description": "Embark on a captivating journey to the Isle of Skye, known for its dramatic landscapes, charming villages, and rich history. Visit the iconic Old Man of Storr, a towering rock formation, explore the magical Fairy Pools with their crystal-clear waters, and discover the historic Dunvegan Castle, the oldest continuously inhabited castle in Scotland.", + "locationName": "Isle of Skye", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scottish-highlands", + "ref": "explore-the-isle-of-skye", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_explore-the-isle-of-skye.jpg" + }, + { + "name": "Attend the Highland Games", + "description": "Immerse yourself in Scottish culture at the Highland Games, a traditional sporting event featuring competitions in caber tossing, hammer throwing, and tug-of-war. Enjoy the lively atmosphere, bagpipe music, Highland dancing, and delicious local food. This is a unique opportunity to experience the spirit and heritage of the Highlands.", + "locationName": "Various locations throughout the Highlands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scottish-highlands", + "ref": "attend-the-highland-games", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_attend-the-highland-games.jpg" + }, + { + "name": "Discover the Mysteries of Loch Ness", + "description": "Embark on a captivating boat tour of the legendary Loch Ness, delving into the folklore and mystery surrounding the elusive Loch Ness Monster. Enjoy breathtaking views of the surrounding landscapes and learn about the history and ecology of this iconic Scottish loch.", + "locationName": "Loch Ness", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "scottish-highlands", + "ref": "discover-the-mysteries-of-loch-ness", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_discover-the-mysteries-of-loch-ness.jpg" + }, + { + "name": "Step Back in Time at Culloden Battlefield", + "description": "Visit the haunting Culloden Battlefield, site of the last Jacobite Rising in 1746. Explore the visitor center to learn about the battle's history and significance, walk the battlefield where the clans clashed, and pay respects at the memorial cairn.", + "locationName": "Culloden Battlefield", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scottish-highlands", + "ref": "step-back-in-time-at-culloden-battlefield", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_step-back-in-time-at-culloden-battlefield.jpg" + }, + { + "name": "Indulge in a Culinary Journey", + "description": "Embark on a delectable food tour, savoring the finest Scottish cuisine. Sample fresh seafood, locally sourced meats, and traditional dishes like haggis, neeps, and tatties. Explore charming cafes, vibrant markets, and acclaimed restaurants, experiencing the unique flavors of the Highlands.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "scottish-highlands", + "ref": "indulge-in-a-culinary-journey", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_indulge-in-a-culinary-journey.jpg" + }, + { + "name": "Go Salmon Fishing on the River Spey", + "description": "Experience the thrill of salmon fishing on the renowned River Spey, one of Scotland's premier fishing destinations. Cast your line amidst stunning scenery and try your hand at catching the 'King of Fish'. Whether you're a seasoned angler or a beginner, a day spent fishing on the Spey is an unforgettable experience.", + "locationName": "River Spey", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "scottish-highlands", + "ref": "go-salmon-fishing-on-the-river-spey", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_go-salmon-fishing-on-the-river-spey.jpg" + }, + { + "name": "Ride the Jacobite Steam Train", + "description": "Embark on a magical journey aboard the Jacobite Steam Train, also known as the 'Hogwarts Express'. Travel through breathtaking landscapes, crossing the iconic Glenfinnan Viaduct featured in the Harry Potter films. Immerse yourself in the nostalgic atmosphere and enjoy panoramic views of the Scottish countryside.", + "locationName": "Fort William to Mallaig", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "scottish-highlands", + "ref": "ride-the-jacobite-steam-train", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_ride-the-jacobite-steam-train.jpg" + }, + { + "name": "Go Canyoning or Gorge Walking", + "description": "Experience the thrill of canyoning or gorge walking in the Scottish Highlands. Plunge into crystal-clear pools, rappel down waterfalls, and navigate through stunning gorges. This adrenaline-pumping activity is perfect for adventure seekers and nature lovers alike.", + "locationName": "Various locations throughout the Highlands", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "scottish-highlands", + "ref": "go-canyoning-or-gorge-walking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_go-canyoning-or-gorge-walking.jpg" + }, + { + "name": "Take a Boat Trip to Spot Marine Wildlife", + "description": "Embark on a boat trip and witness the incredible marine wildlife of the Scottish Highlands. Look out for seals, dolphins, whales, and a variety of seabirds as you cruise along the coastline or explore the islands. This activity is perfect for families and wildlife enthusiasts.", + "locationName": "Various locations along the coast", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scottish-highlands", + "ref": "take-a-boat-trip-to-spot-marine-wildlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_take-a-boat-trip-to-spot-marine-wildlife.jpg" + }, + { + "name": "Visit Urquhart Castle", + "description": "Explore the ruins of Urquhart Castle, perched on the banks of Loch Ness. Discover its fascinating history dating back to the 13th century, admire the stunning views of the loch, and learn about the legendary Loch Ness Monster.", + "locationName": "Drumnadrochit", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "scottish-highlands", + "ref": "visit-urquhart-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_visit-urquhart-castle.jpg" + }, + { + "name": "Enjoy a Traditional Ceilidh", + "description": "Immerse yourself in Scottish culture by attending a traditional ceilidh. Dance the night away to lively folk music, enjoy the friendly atmosphere, and experience a true taste of Highland hospitality.", + "locationName": "Various towns and villages throughout the Highlands", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "scottish-highlands", + "ref": "enjoy-a-traditional-ceilidh", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_enjoy-a-traditional-ceilidh.jpg" + }, + { + "name": "Go Skiing or Snowboarding in the Cairngorms", + "description": "Hit the slopes of the Cairngorms National Park for a thrilling skiing or snowboarding adventure. With a variety of runs for all levels, stunning mountain scenery, and a lively après-ski scene, this is a perfect winter destination for snow sports enthusiasts.", + "locationName": "Cairngorm Mountain", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "scottish-highlands", + "ref": "go-skiing-or-snowboarding-in-the-cairngorms", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/scottish-highlands_go-skiing-or-snowboarding-in-the-cairngorms.jpg" + }, + { + "name": "Explore the Gyeongbokgung Palace", + "description": "Step back in time and immerse yourself in Korean history by visiting the magnificent Gyeongbokgung Palace, the largest and most stunning royal palace in Seoul. Stroll through the grand courtyards, admire the intricate architecture, and witness the changing of the guard ceremony for a glimpse into Korea's regal past. Don't forget to rent a hanbok (traditional Korean dress) for an even more immersive experience and stunning photos!", + "locationName": "Gyeongbokgung Palace", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "seoul", + "ref": "explore-the-gyeongbokgung-palace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_explore-the-gyeongbokgung-palace.jpg" + }, + { + "name": "Discover the Bukchon Hanok Village", + "description": "Wander through the charming Bukchon Hanok Village, a historic neighborhood with traditional Korean houses (hanoks) dating back to the Joseon Dynasty. Get lost in the labyrinthine alleyways, admire the unique architecture, and pop into quaint tea houses and craft shops. This is a perfect opportunity to capture the essence of old Seoul and experience the city's cultural heritage.", + "locationName": "Bukchon Hanok Village", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "seoul", + "ref": "discover-the-bukchon-hanok-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_discover-the-bukchon-hanok-village.jpg" + }, + { + "name": "Indulge in Korean BBQ", + "description": "No trip to Seoul is complete without savoring the mouthwatering flavors of Korean BBQ. Head to the bustling district of Jongno or the trendy Gangnam area to find numerous BBQ restaurants offering a variety of marinated meats, banchan (side dishes), and dipping sauces. Gather your friends or family around a sizzling grill, cook your own food, and experience this quintessential Korean dining tradition.", + "locationName": "Jongno or Gangnam", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "seoul", + "ref": "indulge-in-korean-bbq", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_indulge-in-korean-bbq.jpg" + }, + { + "name": "Experience the Bustling Nightlife of Hongdae", + "description": "When the sun goes down, head to the vibrant district of Hongdae, known for its youthful energy, live music scene, and trendy bars. Catch a performance by up-and-coming indie bands, dance the night away at a club, or simply enjoy a drink with friends at a cozy pub. Hongdae offers a diverse range of nightlife options, ensuring an unforgettable evening.", + "locationName": "Hongdae", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "seoul", + "ref": "experience-the-bustling-nightlife-of-hongdae", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_experience-the-bustling-nightlife-of-hongdae.jpg" + }, + { + "name": "Shop till you drop in Myeongdong", + "description": "For a retail therapy fix, head to Myeongdong, a shopper's paradise with a plethora of stores offering everything from trendy fashion and cosmetics to K-pop merchandise and street food. Explore the bustling streets, discover local and international brands, and indulge in some serious shopping.", + "locationName": "Myeongdong", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "seoul", + "ref": "shop-till-you-drop-in-myeongdong", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_shop-till-you-drop-in-myeongdong.jpg" + }, + { + "name": "Hike to the Peak of Bukhansan National Park", + "description": "Escape the urban bustle and immerse yourself in nature with a hike through Bukhansan National Park. Choose from various trails catering to different fitness levels, leading you to breathtaking panoramic views of the city from the mountain's peak. Pack a picnic and enjoy a serene lunch amidst the stunning scenery.", + "locationName": "Bukhansan National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "seoul", + "ref": "hike-to-the-peak-of-bukhansan-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_hike-to-the-peak-of-bukhansan-national-park.jpg" + }, + { + "name": "Immerse in Korean Art at the Leeum Samsung Museum of Art", + "description": "Delve into the world of Korean art and culture at the renowned Leeum Samsung Museum of Art. Explore a diverse collection of traditional and contemporary works, spanning paintings, sculptures, and digital installations. The museum's unique architecture and serene atmosphere offer a captivating experience for art enthusiasts.", + "locationName": "Leeum Samsung Museum of Art", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "seoul", + "ref": "immerse-in-korean-art-at-the-leeum-samsung-museum-of-art", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_immerse-in-korean-art-at-the-leeum-samsung-museum-of-art.jpg" + }, + { + "name": "Cruise Along the Han River", + "description": "Embark on a relaxing cruise along the picturesque Han River and witness Seoul's skyline from a unique perspective. Enjoy stunning views of iconic landmarks, including the N Seoul Tower and the Banpo Bridge Rainbow Fountain Show. Opt for a romantic evening cruise or a family-friendly daytime excursion.", + "locationName": "Han River", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "seoul", + "ref": "cruise-along-the-han-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_cruise-along-the-han-river.jpg" + }, + { + "name": "Discover Serenity at Bongeunsa Temple", + "description": "Escape the city's hustle and bustle and find tranquility at the historic Bongeunsa Temple. Explore the temple's intricate architecture, participate in a traditional tea ceremony, or join a Templestay program for a deeper immersion into Korean Buddhism and culture.", + "locationName": "Bongeunsa Temple", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "seoul", + "ref": "discover-serenity-at-bongeunsa-temple", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_discover-serenity-at-bongeunsa-temple.jpg" + }, + { + "name": "Experience the Thrill of Lotte World", + "description": "Embark on an exciting adventure at Lotte World, a renowned amusement park featuring thrilling rides, enchanting parades, and a variety of entertainment options. Explore the indoor Adventure Park or experience the magic of the outdoor Magic Island. Lotte World offers fun for all ages, making it a perfect family-friendly destination.", + "locationName": "Lotte World", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "seoul", + "ref": "experience-the-thrill-of-lotte-world", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_experience-the-thrill-of-lotte-world.jpg" + }, + { + "name": "Stroll Through the Secret Garden", + "description": "Escape the urban bustle and discover the serene beauty of the Secret Garden, also known as Huwon, nestled within the Changdeokgung Palace complex. Explore hidden pavilions, lotus ponds, and landscaped gardens that offer a tranquil oasis in the heart of the city. This UNESCO World Heritage site provides a glimpse into the history and artistry of Korean landscape design.", + "locationName": "Changdeokgung Palace", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "seoul", + "ref": "stroll-through-the-secret-garden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_stroll-through-the-secret-garden.jpg" + }, + { + "name": "Delve into Korean History at the National Museum", + "description": "Embark on a journey through Korea's rich history and cultural heritage at the National Museum of Korea. Explore extensive collections of artifacts, from ancient relics to contemporary art, showcasing the country's fascinating evolution. With interactive exhibits and diverse galleries, the museum offers a captivating experience for history buffs and curious minds alike.", + "locationName": "National Museum of Korea", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "seoul", + "ref": "delve-into-korean-history-at-the-national-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_delve-into-korean-history-at-the-national-museum.jpg" + }, + { + "name": "Unwind and Rejuvenate at a Traditional Korean Spa", + "description": "Indulge in a unique cultural experience and revitalize your body and mind at a traditional Korean spa, known as a jjimjilbang. Enjoy a variety of saunas, hot tubs, and therapeutic treatments, including body scrubs and massages. These 24-hour facilities often offer additional amenities like sleeping areas, restaurants, and entertainment, making it a perfect place to relax and unwind.", + "locationName": "Various jjimjilbangs throughout the city", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "seoul", + "ref": "unwind-and-rejuvenate-at-a-traditional-korean-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_unwind-and-rejuvenate-at-a-traditional-korean-spa.jpg" + }, + { + "name": "Wander Through the Colorful Ihwa Mural Village", + "description": "Step into a world of art and creativity at the Ihwa Mural Village. This once-declining neighborhood has been transformed into an open-air gallery with vibrant murals and art installations adorning the walls and streets. Explore the winding alleyways, capture Instagram-worthy photos, and discover hidden cafes and shops along the way.", + "locationName": "Ihwa Mural Village", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "seoul", + "ref": "wander-through-the-colorful-ihwa-mural-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_wander-through-the-colorful-ihwa-mural-village.jpg" + }, + { + "name": "Catch a Performance at the Korea House", + "description": "Immerse yourself in Korean culture with a captivating performance at the Korea House. Enjoy traditional music and dance shows, showcasing the elegance and vibrancy of Korean performing arts. The venue also offers cultural workshops and authentic Korean dining experiences, providing a comprehensive cultural immersion.", + "locationName": "Korea House", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "seoul", + "ref": "catch-a-performance-at-the-korea-house", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_catch-a-performance-at-the-korea-house.jpg" + }, + { + "name": "Explore the Trendy Gangnam District", + "description": "Immerse yourself in the upscale and trendy Gangnam district, known for its luxury boutiques, high-end restaurants, and vibrant nightlife. Discover the latest fashion trends, indulge in delicious cuisine, and experience the glamorous side of Seoul.", + "locationName": "Gangnam District", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "seoul", + "ref": "explore-the-trendy-gangnam-district", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_explore-the-trendy-gangnam-district.jpg" + }, + { + "name": "Visit the Demilitarized Zone (DMZ)", + "description": "Embark on a unique journey to the Demilitarized Zone (DMZ), the border between North and South Korea. Learn about the Korean War, witness the remnants of the conflict, and gain insights into the complex history of the peninsula.", + "locationName": "Demilitarized Zone (DMZ)", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 2, + "destinationRef": "seoul", + "ref": "visit-the-demilitarized-zone-dmz-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_visit-the-demilitarized-zone-dmz-.jpg" + }, + { + "name": "Go on a Culinary Adventure at Gwangjang Market", + "description": "Embark on a culinary adventure at Gwangjang Market, a bustling traditional market filled with an array of street food vendors and local delicacies. Sample authentic Korean dishes, from savory kimchi pancakes to sweet hotteok, and experience the vibrant atmosphere of this culinary paradise.", + "locationName": "Gwangjang Market", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "seoul", + "ref": "go-on-a-culinary-adventure-at-gwangjang-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_go-on-a-culinary-adventure-at-gwangjang-market.jpg" + }, + { + "name": "Enjoy Panoramic Views from N Seoul Tower", + "description": "Ascend to the top of N Seoul Tower, an iconic landmark offering breathtaking panoramic views of the city. Take in the stunning cityscape, enjoy romantic moments with your loved ones, and create lasting memories at this must-visit attraction.", + "locationName": "N Seoul Tower", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "seoul", + "ref": "enjoy-panoramic-views-from-n-seoul-tower", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_enjoy-panoramic-views-from-n-seoul-tower.jpg" + }, + { + "name": "Take a Day Trip to the UNESCO World Heritage Site of Suwon Hwaseong Fortress", + "description": "Embark on a day trip to Suwon Hwaseong Fortress, a UNESCO World Heritage Site renowned for its impressive architecture and historical significance. Explore the fortress walls, palaces, and temples, and delve into the rich history of the Joseon Dynasty.", + "locationName": "Suwon Hwaseong Fortress", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "seoul", + "ref": "take-a-day-trip-to-the-unesco-world-heritage-site-of-suwon-hwaseong-fortress", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seoul_take-a-day-trip-to-the-unesco-world-heritage-site-of-suwon-hwaseong-fortress.jpg" + }, + { + "name": "Witness the Great Migration", + "description": "Embark on an unforgettable safari experience to witness the awe-inspiring Great Migration, where millions of wildebeest, zebras, and other herbivores thunder across the plains in search of fresh grazing. Watch in amazement as they navigate treacherous river crossings, creating a spectacle of nature's raw power and determination. This iconic event offers exceptional wildlife viewing and photography opportunities.", + "locationName": "Serengeti National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "serengeti-national-park", + "ref": "witness-the-great-migration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_witness-the-great-migration.jpg" + }, + { + "name": "Hot Air Balloon Safari", + "description": "Take to the skies in a hot air balloon and soar above the vast Serengeti plains at sunrise. Enjoy breathtaking panoramic views of the diverse landscapes, spot wildlife from a unique perspective, and capture stunning aerial photographs. This serene and unforgettable experience offers a different way to appreciate the beauty and grandeur of the Serengeti.", + "locationName": "Serengeti National Park", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "serengeti-national-park", + "ref": "hot-air-balloon-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_hot-air-balloon-safari.jpg" + }, + { + "name": "Game Drives and Wildlife Viewing", + "description": "Embark on thrilling game drives through the Serengeti's diverse ecosystems, accompanied by experienced guides. Encounter an array of wildlife, including lions, elephants, giraffes, leopards, cheetahs, and numerous bird species. Learn about their behavior, habitats, and the delicate balance of the ecosystem. Day and night game drives offer unique opportunities to observe nocturnal animals and predators on the hunt.", + "locationName": "Serengeti National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "serengeti-national-park", + "ref": "game-drives-and-wildlife-viewing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_game-drives-and-wildlife-viewing.jpg" + }, + { + "name": "Cultural Encounters with the Maasai People", + "description": "Immerse yourself in the rich culture and traditions of the Maasai people, who have lived in harmony with the Serengeti's wildlife for centuries. Visit a Maasai village, interact with the locals, and learn about their unique way of life, including their customs, dress, and traditional dances. Gain insights into their deep connection with the land and their knowledge of the natural world.", + "locationName": "Maasai Villages", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "serengeti-national-park", + "ref": "cultural-encounters-with-the-maasai-people", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_cultural-encounters-with-the-maasai-people.jpg" + }, + { + "name": "Bushwalking and Nature Trails", + "description": "For a more intimate experience with the Serengeti's landscapes, embark on guided bushwalks and nature trails. Explore the diverse flora and fauna, learn about medicinal plants, and discover hidden gems within the park. This activity allows you to connect with nature on a deeper level and appreciate the smaller details of the ecosystem.", + "locationName": "Serengeti National Park", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "serengeti-national-park", + "ref": "bushwalking-and-nature-trails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_bushwalking-and-nature-trails.jpg" + }, + { + "name": "Birdwatching in the Serengeti", + "description": "Embark on a guided birdwatching tour to discover the rich avian diversity of the Serengeti. With over 500 bird species, including ostriches, secretary birds, and various raptors, the park offers a paradise for bird enthusiasts. Capture stunning photos and learn about the fascinating behaviors of these feathered creatures.", + "locationName": "Various locations within the park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "serengeti-national-park", + "ref": "birdwatching-in-the-serengeti", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_birdwatching-in-the-serengeti.jpg" + }, + { + "name": "Hot Air Balloon Safari at Dawn", + "description": "Experience the breathtaking beauty of the Serengeti from a unique perspective with a hot air balloon safari at dawn. As the sun rises over the vast plains, witness the spectacular landscape and observe wildlife from above. This unforgettable experience offers stunning panoramic views and a sense of tranquility.", + "locationName": "Designated launch sites within the park", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "serengeti-national-park", + "ref": "hot-air-balloon-safari-at-dawn", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_hot-air-balloon-safari-at-dawn.jpg" + }, + { + "name": "Photography Safari", + "description": "Join a specialized photography safari designed for both amateur and professional photographers. Capture incredible images of wildlife, landscapes, and the unique moments that unfold in the Serengeti. Learn valuable tips from experienced guides and create lasting memories through your lens.", + "locationName": "Various locations within the park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "serengeti-national-park", + "ref": "photography-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_photography-safari.jpg" + }, + { + "name": "Stargazing in the Serengeti", + "description": "Escape the city lights and immerse yourself in the wonders of the night sky. Join a stargazing experience led by knowledgeable guides who will share insights about constellations, planets, and the Milky Way. Marvel at the brilliance of the stars and enjoy the tranquility of the African night.", + "locationName": "Designated areas away from light pollution", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "serengeti-national-park", + "ref": "stargazing-in-the-serengeti", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_stargazing-in-the-serengeti.jpg" + }, + { + "name": "Bush Dinner Under the Stars", + "description": "Indulge in a romantic and unforgettable bush dinner experience under the starlit sky. Enjoy a delicious meal prepared with local flavors, surrounded by the sights and sounds of the African wilderness. This intimate setting provides a unique opportunity to connect with nature and create lasting memories.", + "locationName": "Private campsites or lodges within the park", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "serengeti-national-park", + "ref": "bush-dinner-under-the-stars", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_bush-dinner-under-the-stars.jpg" + }, + { + "name": "Visit the Serengeti Visitor Center", + "description": "Embark on an educational journey at the Serengeti Visitor Center, where interactive exhibits and informative displays delve into the park's diverse ecosystem, wildlife, and conservation efforts. Learn about the fascinating history of the Serengeti, the science behind the Great Migration, and the challenges faced by the park's inhabitants.", + "locationName": "Serengeti National Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "serengeti-national-park", + "ref": "visit-the-serengeti-visitor-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_visit-the-serengeti-visitor-center.jpg" + }, + { + "name": "Explore the Grumeti River", + "description": "Embark on a scenic boat trip along the Grumeti River, a haven for wildlife and a prime location to witness crocodiles basking on the banks, hippos wallowing in the water, and a variety of bird species soaring overhead. Keep an eye out for elephants, giraffes, and other animals that come to the river to quench their thirst.", + "locationName": "Grumeti River", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "serengeti-national-park", + "ref": "explore-the-grumeti-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_explore-the-grumeti-river.jpg" + }, + { + "name": "Visit a Maasai Village", + "description": "Immerse yourself in the rich culture of the Maasai people by visiting a traditional Maasai village. Engage with the locals, learn about their customs and traditions, witness their vibrant dances, and admire their intricate beadwork and craftsmanship. This cultural encounter offers a unique opportunity to gain insights into the Maasai way of life.", + "locationName": "Maasai Village", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "serengeti-national-park", + "ref": "visit-a-maasai-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_visit-a-maasai-village.jpg" + }, + { + "name": "Go on a Walking Safari", + "description": "For a more intimate wildlife experience, embark on a guided walking safari with an experienced ranger. Explore the Serengeti's diverse landscapes on foot, getting closer to nature and observing smaller creatures, plants, and tracks that might be missed on a traditional game drive. This adventurous activity allows you to connect with the Serengeti's ecosystem on a deeper level.", + "locationName": "Serengeti National Park", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "serengeti-national-park", + "ref": "go-on-a-walking-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_go-on-a-walking-safari.jpg" + }, + { + "name": "Enjoy a Sundowner Cocktail", + "description": "As the sun begins its descent, indulge in a relaxing sundowner experience amidst the breathtaking scenery of the Serengeti. Sip on a refreshing cocktail or a glass of wine while witnessing the vibrant colors of the African sunset paint the sky. This tranquil moment allows you to reflect on the day's adventures and appreciate the beauty of the natural surroundings.", + "locationName": "Serengeti National Park", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "serengeti-national-park", + "ref": "enjoy-a-sundowner-cocktail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_enjoy-a-sundowner-cocktail.jpg" + }, + { + "name": "Horseback Riding Safari", + "description": "Experience the thrill of exploring the Serengeti plains on horseback, getting up close to wildlife while enjoying the freedom and serenity of this unique mode of transport. Skilled guides will lead you through diverse landscapes, allowing you to observe animals in their natural habitat from a different perspective.", + "locationName": "Serengeti National Park", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "serengeti-national-park", + "ref": "horseback-riding-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_horseback-riding-safari.jpg" + }, + { + "name": "Off-Road Adventure to the Gol Mountains", + "description": "Embark on an exhilarating off-road adventure to the remote Gol Mountains, located on the park's eastern edge. Discover hidden waterfalls, ancient rock paintings, and breathtaking panoramic views of the Serengeti plains. This rugged journey offers a unique perspective of the park's diverse ecosystem.", + "locationName": "Gol Mountains", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "serengeti-national-park", + "ref": "off-road-adventure-to-the-gol-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_off-road-adventure-to-the-gol-mountains.jpg" + }, + { + "name": "Nighttime Wildlife Safari", + "description": "Venture into the Serengeti under the cover of darkness on a thrilling nighttime wildlife safari. Equipped with spotlights, you'll have the opportunity to witness the nocturnal behavior of animals rarely seen during the day, including predators on the hunt and elusive creatures like aardvarks and bushbabies.", + "locationName": "Serengeti National Park", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 4, + "destinationRef": "serengeti-national-park", + "ref": "nighttime-wildlife-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_nighttime-wildlife-safari.jpg" + }, + { + "name": "Learn Bushcraft and Survival Skills", + "description": "Immerse yourself in the wilderness and learn essential bushcraft and survival skills from experienced guides. Discover how to identify edible plants, build a fire, navigate using the stars, and create basic shelters. This hands-on experience will connect you with nature and equip you with valuable knowledge for future adventures.", + "locationName": "Serengeti National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "serengeti-national-park", + "ref": "learn-bushcraft-and-survival-skills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_learn-bushcraft-and-survival-skills.jpg" + }, + { + "name": "Volunteer at a Wildlife Conservation Project", + "description": "Contribute to the preservation of the Serengeti ecosystem by volunteering at a wildlife conservation project. Opportunities may include assisting with animal monitoring, habitat restoration, community outreach, or anti-poaching initiatives. This immersive experience allows you to make a positive impact while gaining insights into the challenges and rewards of conservation work.", + "locationName": "Serengeti National Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "serengeti-national-park", + "ref": "volunteer-at-a-wildlife-conservation-project", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/serengeti-national-park_volunteer-at-a-wildlife-conservation-project.jpg" + }, + { + "name": "Explore the Alcázar Palace", + "description": "Step back in time at the Alcázar, a magnificent Moorish palace with stunning architecture, lush gardens, and captivating history. Explore the intricate courtyards, admire the ornate tilework, and immerse yourself in the royal ambiance of this UNESCO World Heritage Site.", + "locationName": "Alcázar of Seville", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "seville", + "ref": "explore-the-alc-zar-palace", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_explore-the-alc-zar-palace.jpg" + }, + { + "name": "Experience the Passion of Flamenco", + "description": "Witness the soul-stirring art of flamenco in a traditional tablao. Be mesmerized by the passionate dance moves, vibrant costumes, and rhythmic music that embodies the spirit of Andalusian culture. Feel the energy and emotion as the dancers and musicians create an unforgettable spectacle.", + "locationName": "Museo del Baile Flamenco or Tablao El Arenal", + "duration": 1.5, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "seville", + "ref": "experience-the-passion-of-flamenco", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_experience-the-passion-of-flamenco.jpg" + }, + { + "name": "Wander Through the Santa Cruz District", + "description": "Get lost in the enchanting maze of narrow streets and charming squares in the Santa Cruz district, the historic Jewish quarter. Discover hidden patios, admire the whitewashed houses adorned with colorful flowers, and soak up the vibrant atmosphere. Enjoy tapas at a local bar and experience the authentic Sevillian lifestyle.", + "locationName": "Santa Cruz district", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "seville", + "ref": "wander-through-the-santa-cruz-district", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_wander-through-the-santa-cruz-district.jpg" + }, + { + "name": "Indulge in a Tapas Crawl", + "description": "Embark on a culinary adventure through Seville's tapas scene. Hop from bar to bar, savoring a variety of small plates bursting with flavors. From classic tapas like patatas bravas and jamón ibérico to innovative creations, experience the city's gastronomic delights and lively atmosphere.", + "locationName": "Various tapas bars in Seville", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "seville", + "ref": "indulge-in-a-tapas-crawl", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_indulge-in-a-tapas-crawl.jpg" + }, + { + "name": "Cruise Along the Guadalquivir River", + "description": "Enjoy a relaxing boat trip along the Guadalquivir River, admiring Seville's iconic landmarks from a different perspective. See the Torre del Oro, the Triana Bridge, and the Plaza de Toros de la Maestranza as you glide along the water, soaking up the scenic views and gentle breeze.", + "locationName": "Guadalquivir River", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "seville", + "ref": "cruise-along-the-guadalquivir-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_cruise-along-the-guadalquivir-river.jpg" + }, + { + "name": "Bike Tour Through Seville's Charms", + "description": "Embark on a leisurely bike tour through Seville's charming streets and hidden corners. Discover local life, historical landmarks, and picturesque squares at your own pace. This family-friendly activity is a fantastic way to see the city from a different perspective and enjoy the fresh air.", + "locationName": "Seville City Center", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "seville", + "ref": "bike-tour-through-seville-s-charms", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_bike-tour-through-seville-s-charms.jpg" + }, + { + "name": "Explore the Metropol Parasol", + "description": "Ascend the Metropol Parasol, a modern wooden structure offering breathtaking panoramic views of Seville. Wander along its elevated walkways, admire the unique architecture, and capture stunning photos of the cityscape below. Enjoy a drink or a meal at the rooftop restaurant for a truly memorable experience.", + "locationName": "Metropol Parasol", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "seville", + "ref": "explore-the-metropol-parasol", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_explore-the-metropol-parasol.jpg" + }, + { + "name": "Shop at the Seville Flea Market", + "description": "Immerse yourself in the vibrant atmosphere of the Seville Flea Market (El Jueves). Browse through a treasure trove of antiques, vintage clothing, handcrafted souvenirs, and unique local finds. This bustling market offers a glimpse into Seville's culture and is a paradise for bargain hunters.", + "locationName": "Calle Feria", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "seville", + "ref": "shop-at-the-seville-flea-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_shop-at-the-seville-flea-market.jpg" + }, + { + "name": "Catch a Show at the Maestranza Bullring", + "description": "Experience the thrill and artistry of a traditional bullfight at the Maestranza Bullring, one of the oldest and most renowned bullrings in Spain. Witness the skill of the matadors and the pageantry of this cultural spectacle. Please note that opinions on bullfighting vary, and it may not be suitable for all audiences.", + "locationName": "Real Maestranza Bullring", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "seville", + "ref": "catch-a-show-at-the-maestranza-bullring", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_catch-a-show-at-the-maestranza-bullring.jpg" + }, + { + "name": "Relax at Maria Luisa Park", + "description": "Escape the city bustle and unwind in the tranquil Maria Luisa Park. Stroll through its beautiful gardens, admire the fountains and monuments, and rent a boat for a relaxing ride on the lake. This expansive green space is perfect for picnics, leisurely walks, and enjoying the serene atmosphere.", + "locationName": "Maria Luisa Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "seville", + "ref": "relax-at-maria-luisa-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_relax-at-maria-luisa-park.jpg" + }, + { + "name": "Day Trip to Cordoba", + "description": "Embark on a captivating journey to the historical city of Cordoba, a UNESCO World Heritage Site. Explore the Mezquita, a magnificent mosque-cathedral showcasing a captivating blend of Islamic and Christian architecture. Stroll through the enchanting Jewish Quarter with its narrow streets and charming patios, and immerse yourself in the rich cultural tapestry of this ancient city.", + "locationName": "Cordoba", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "seville", + "ref": "day-trip-to-cordoba", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_day-trip-to-cordoba.jpg" + }, + { + "name": "Kayaking on the Guadalquivir River", + "description": "Experience Seville from a different perspective with a thrilling kayaking adventure on the Guadalquivir River. Paddle along the picturesque waterway, passing iconic landmarks and enjoying the refreshing breeze. This unique activity offers a blend of exercise, sightseeing, and a chance to connect with nature.", + "locationName": "Guadalquivir River", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "seville", + "ref": "kayaking-on-the-guadalquivir-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_kayaking-on-the-guadalquivir-river.jpg" + }, + { + "name": "Flamenco Dance Class", + "description": "Immerse yourself in the passionate world of flamenco with an engaging dance class. Learn the basic steps and rhythms of this traditional Spanish art form from experienced instructors. Whether you're a beginner or have some dance experience, this activity offers a fun and cultural way to connect with the soul of Seville.", + "locationName": "Various dance studios throughout Seville", + "duration": 1.5, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "seville", + "ref": "flamenco-dance-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_flamenco-dance-class.jpg" + }, + { + "name": "Rooftop Bar Hopping", + "description": "Savor stunning panoramic views of Seville while enjoying the city's vibrant nightlife scene. Embark on a rooftop bar hopping adventure, discovering hidden gems and popular hotspots with breathtaking vistas. Sip on delicious cocktails, soak in the lively atmosphere, and create unforgettable memories under the starry sky.", + "locationName": "Various rooftop bars throughout Seville", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "seville", + "ref": "rooftop-bar-hopping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_rooftop-bar-hopping.jpg" + }, + { + "name": "Cooking Class", + "description": "Delve into the culinary delights of Seville with a hands-on cooking class. Learn the secrets of traditional Andalusian cuisine from local chefs, mastering the art of preparing tapas, paella, or other regional specialties. This immersive experience allows you to bring a taste of Seville home with you.", + "locationName": "Various cooking schools throughout Seville", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "seville", + "ref": "cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_cooking-class.jpg" + }, + { + "name": "Horse-Drawn Carriage Ride", + "description": "Indulge in a romantic and leisurely horse-drawn carriage ride through the enchanting streets of Seville. Discover hidden corners, admire iconic landmarks, and experience the city's charm in a unique and unforgettable way.", + "locationName": "Seville City Center", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "seville", + "ref": "horse-drawn-carriage-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_horse-drawn-carriage-ride.jpg" + }, + { + "name": "Museo del Baile Flamenco", + "description": "Immerse yourself in the passionate world of flamenco at the Museo del Baile Flamenco. Explore the history, artistry, and cultural significance of this captivating dance form through exhibits, live performances, and interactive displays.", + "locationName": "Museo del Baile Flamenco", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "seville", + "ref": "museo-del-baile-flamenco", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_museo-del-baile-flamenco.jpg" + }, + { + "name": "Triana Market", + "description": "Step into the vibrant atmosphere of the Triana Market, a local gem bursting with colors, aromas, and flavors. Browse stalls overflowing with fresh produce, regional specialties, and handcrafted souvenirs, and immerse yourself in the authentic Sevillian way of life.", + "locationName": "Triana neighborhood", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "seville", + "ref": "triana-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_triana-market.jpg" + }, + { + "name": "Plaza de Toros de la Maestranza Tour", + "description": "Delve into the history and tradition of bullfighting with a guided tour of the Plaza de Toros de la Maestranza. Explore the bullring's museum, learn about the art of bullfighting, and step onto the hallowed sands of the arena where legendary matadors have faced their challenges.", + "locationName": "Plaza de Toros de la Maestranza", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "seville", + "ref": "plaza-de-toros-de-la-maestranza-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_plaza-de-toros-de-la-maestranza-tour.jpg" + }, + { + "name": "Arab Baths Experience", + "description": "Escape the bustling city and indulge in a rejuvenating experience at a traditional Arab bath. Immerse yourself in the soothing waters, enjoy a relaxing massage, and step back in time to the era of Moorish luxury and tranquility.", + "locationName": "Aire de Sevilla", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "seville", + "ref": "arab-baths-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/seville_arab-baths-experience.jpg" + }, + { + "name": "Gardens by the Bay", + "description": "Immerse yourself in the futuristic beauty of Gardens by the Bay, a stunning nature park with iconic Supertrees, diverse plant life, and breathtaking waterfront views. Explore the Flower Dome, Cloud Forest, and OCBC Skyway for a unique and unforgettable experience.", + "locationName": "Gardens by the Bay", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "singapore", + "ref": "gardens-by-the-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_gardens-by-the-bay.jpg" + }, + { + "name": "Hawker Center Food Tour", + "description": "Embark on a culinary adventure through Singapore's renowned hawker centers. Sample a variety of delicious and affordable local dishes, from Hainanese chicken rice to chili crab, and experience the vibrant atmosphere of these bustling food havens.", + "locationName": "Various hawker centers", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "singapore", + "ref": "hawker-center-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_hawker-center-food-tour.jpg" + }, + { + "name": "Sentosa Island Escape", + "description": "Escape to the island resort of Sentosa and enjoy a day of fun and relaxation. Relax on pristine beaches, experience thrilling rides at Universal Studios Singapore, or explore the S.E.A Aquarium, home to a diverse array of marine life.", + "locationName": "Sentosa Island", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "singapore", + "ref": "sentosa-island-escape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_sentosa-island-escape.jpg" + }, + { + "name": "Little India Exploration", + "description": "Step into the vibrant neighborhood of Little India and experience a sensory feast. Wander through colorful streets, admire ornate temples, and discover authentic Indian cuisine, spices, and textiles.", + "locationName": "Little India", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "singapore", + "ref": "little-india-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_little-india-exploration.jpg" + }, + { + "name": "Singapore River Cruise", + "description": "Take a scenic cruise along the Singapore River and admire the city's iconic landmarks from a unique perspective. Enjoy stunning views of Marina Bay Sands, Merlion Park, and the colonial district while learning about Singapore's history and culture.", + "locationName": "Singapore River", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "singapore", + "ref": "singapore-river-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_singapore-river-cruise.jpg" + }, + { + "name": "Night Safari Adventure", + "description": "Embark on a thrilling tram ride through the world's first nocturnal wildlife park, encountering over 2,500 animals from around the globe in their natural habitats. Witness the magic of the jungle come alive under the starry sky as you observe fascinating creatures like lions, tigers, elephants, and more.", + "locationName": "Night Safari Singapore", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "singapore", + "ref": "night-safari-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_night-safari-adventure.jpg" + }, + { + "name": "Peranakan Museum Exploration", + "description": "Delve into the rich heritage of the Peranakan community, descendants of Chinese immigrants who settled in Southeast Asia centuries ago. Admire intricate artifacts, textiles, and jewelry, and learn about their unique cultural traditions, cuisine, and language.", + "locationName": "Peranakan Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "singapore", + "ref": "peranakan-museum-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_peranakan-museum-exploration.jpg" + }, + { + "name": "Chinatown Walking Tour", + "description": "Immerse yourself in the vibrant atmosphere of Chinatown, with its colorful shophouses, bustling markets, and ornate temples. Discover hidden gems, sample delicious street food, and learn about the history and traditions of Singapore's Chinese community.", + "locationName": "Chinatown", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "singapore", + "ref": "chinatown-walking-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_chinatown-walking-tour.jpg" + }, + { + "name": "MacRitchie Reservoir Hike", + "description": "Escape the city bustle and reconnect with nature on a scenic hike through the lush rainforest surrounding MacRitchie Reservoir. Cross the iconic TreeTop Walk suspension bridge, spot diverse wildlife, and enjoy breathtaking views of the reservoir and surrounding greenery.", + "locationName": "MacRitchie Reservoir Park", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "singapore", + "ref": "macritchie-reservoir-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_macritchie-reservoir-hike.jpg" + }, + { + "name": "Orchard Road Shopping Spree", + "description": "Indulge in a retail therapy session along Orchard Road, Singapore's renowned shopping district. Explore a vast array of luxury brands, flagship stores, and department stores, offering everything from high-end fashion and electronics to local souvenirs and unique finds.", + "locationName": "Orchard Road", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "singapore", + "ref": "orchard-road-shopping-spree", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_orchard-road-shopping-spree.jpg" + }, + { + "name": "Jurong Bird Park Adventure", + "description": "Embark on a colorful journey through Jurong Bird Park, home to over 5,000 birds from around the world. Witness captivating bird shows, stroll through walk-in aviaries, and get up close to vibrant species like flamingos, penguins, and parrots. This immersive experience offers a perfect blend of education and entertainment for all ages.", + "locationName": "Jurong Bird Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "singapore", + "ref": "jurong-bird-park-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_jurong-bird-park-adventure.jpg" + }, + { + "name": "Pulau Ubin Cycling Escape", + "description": "Escape the urban bustle and discover the rustic charm of Pulau Ubin, a tranquil island off Singapore's coast. Rent a bike and explore its idyllic landscapes, including lush forests, serene beaches, and traditional kampongs (villages). Immerse yourself in the island's laid-back atmosphere and enjoy a unique glimpse into Singapore's past.", + "locationName": "Pulau Ubin", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "singapore", + "ref": "pulau-ubin-cycling-escape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_pulau-ubin-cycling-escape.jpg" + }, + { + "name": "Singapore Botanic Gardens Stroll", + "description": "Unwind amidst the verdant beauty of the Singapore Botanic Gardens, a UNESCO World Heritage Site. Wander through themed gardens, admire the National Orchid Garden's stunning collection, and enjoy a peaceful picnic surrounded by nature. This tranquil oasis offers a welcome respite from the city's vibrant energy.", + "locationName": "Singapore Botanic Gardens", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "singapore", + "ref": "singapore-botanic-gardens-stroll", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_singapore-botanic-gardens-stroll.jpg" + }, + { + "name": "Marina Bay Sands SkyPark Observation Deck", + "description": "Experience breathtaking panoramic views of Singapore's skyline from the Marina Bay Sands SkyPark Observation Deck. Capture iconic landmarks like the Supertree Grove, the Singapore Flyer, and the city's dazzling skyscrapers. This vantage point offers an unforgettable perspective of the city's architectural marvels and vibrant cityscape.", + "locationName": "Marina Bay Sands SkyPark", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "singapore", + "ref": "marina-bay-sands-skypark-observation-deck", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_marina-bay-sands-skypark-observation-deck.jpg" + }, + { + "name": "Fort Canning Park History and Heritage", + "description": "Step back in time and explore the rich history of Singapore at Fort Canning Park. Discover ancient artifacts, delve into the park's colonial past, and witness remnants of the spice trade. Immerse yourself in the cultural significance of this landmark and gain a deeper understanding of Singapore's heritage.", + "locationName": "Fort Canning Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "singapore", + "ref": "fort-canning-park-history-and-heritage", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_fort-canning-park-history-and-heritage.jpg" + }, + { + "name": "Singapore Zoo Breakfast with Orangutans", + "description": "Embark on a unique wildlife experience by enjoying a delightful breakfast buffet in the company of playful orangutans at the renowned Singapore Zoo. Witness these incredible creatures swinging through their habitat and learn about their conservation efforts.", + "locationName": "Singapore Zoo", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "singapore", + "ref": "singapore-zoo-breakfast-with-orangutans", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_singapore-zoo-breakfast-with-orangutans.jpg" + }, + { + "name": "Haji Lane Street Art Exploration", + "description": "Immerse yourself in the vibrant street art scene of Haji Lane. Wander through this trendy alleyway, adorned with colorful murals, quirky shops, and hip cafes. Capture Instagram-worthy photos and soak up the artistic atmosphere.", + "locationName": "Haji Lane", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "singapore", + "ref": "haji-lane-street-art-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_haji-lane-street-art-exploration.jpg" + }, + { + "name": "Singapore Cable Car Sky Pass", + "description": "Enjoy breathtaking panoramic views of the city skyline and Sentosa Island from the Singapore Cable Car. Take a scenic ride and hop off at different stations to explore attractions like Mount Faber Park and Sentosa's beaches.", + "locationName": "Singapore Cable Car", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "singapore", + "ref": "singapore-cable-car-sky-pass", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_singapore-cable-car-sky-pass.jpg" + }, + { + "name": "Catch a Show at the Esplanade", + "description": "Experience the vibrant arts scene at the iconic Esplanade - Theatres on the Bay. Choose from a diverse range of performances, including theater, music, dance, and more. Enjoy a world-class show in a stunning architectural setting.", + "locationName": "Esplanade - Theatres on the Bay", + "duration": 2.5, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "singapore", + "ref": "catch-a-show-at-the-esplanade", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_catch-a-show-at-the-esplanade.jpg" + }, + { + "name": "Sungei Buloh Wetland Reserve", + "description": "Escape the city bustle and explore the serene Sungei Buloh Wetland Reserve. Walk along the mangroves, observe diverse birdlife, and learn about the importance of wetland conservation. This is a perfect opportunity to connect with nature and enjoy some peace and quiet.", + "locationName": "Sungei Buloh Wetland Reserve", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "singapore", + "ref": "sungei-buloh-wetland-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/singapore_sungei-buloh-wetland-reserve.jpg" + }, + { + "name": "Lake Bled Boat Ride and Bled Island Visit", + "description": "Embark on a traditional pletna boat ride across the emerald waters of Lake Bled. Visit the iconic Bled Island, climb the 99 steps to reach the Church of the Assumption, and ring the wishing bell for good luck. Enjoy breathtaking views of the Julian Alps and the surrounding countryside.", + "locationName": "Lake Bled", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "slovenia", + "ref": "lake-bled-boat-ride-and-bled-island-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_lake-bled-boat-ride-and-bled-island-visit.jpg" + }, + { + "name": "Postojna Cave Adventure", + "description": "Descend into the mesmerizing Postojna Cave, one of the largest karst cave systems in Europe. Take a thrilling train ride through the caverns and marvel at the stunning stalactites, stalagmites, and otherworldly formations. Learn about the cave's history and its unique ecosystem, including the endemic olm, a blind salamander.", + "locationName": "Postojna Cave", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "slovenia", + "ref": "postojna-cave-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_postojna-cave-adventure.jpg" + }, + { + "name": "Hiking in Triglav National Park", + "description": "Explore the pristine wilderness of Triglav National Park, named after Mount Triglav, Slovenia's highest peak. Choose from a variety of hiking trails, ranging from easy walks to challenging climbs. Discover alpine meadows, glacial valleys, crystal-clear lakes, and breathtaking waterfalls. Keep an eye out for diverse wildlife, including ibex, chamois, and golden eagles.", + "locationName": "Triglav National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "slovenia", + "ref": "hiking-in-triglav-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_hiking-in-triglav-national-park.jpg" + }, + { + "name": "Ljubljana City Tour", + "description": "Wander through the charming streets of Ljubljana, Slovenia's capital city. Admire the Baroque and Art Nouveau architecture, visit the Ljubljana Castle for panoramic views, and stroll along the Ljubljanica River. Explore the vibrant Prešeren Square, the central market, and the picturesque Triple Bridge. Discover the city's rich history and cultural heritage.", + "locationName": "Ljubljana", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "slovenia", + "ref": "ljubljana-city-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_ljubljana-city-tour.jpg" + }, + { + "name": "Slovenian Wine Tasting", + "description": "Indulge in a delightful wine tasting experience in one of Slovenia's renowned wine regions. Sample a variety of local wines, from crisp whites to full-bodied reds, and learn about the country's winemaking traditions. Visit a local vineyard, meet the winemakers, and savor the unique flavors of Slovenian wine.", + "locationName": "Various wine regions", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "slovenia", + "ref": "slovenian-wine-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_slovenian-wine-tasting.jpg" + }, + { + "name": "Whitewater Rafting on the Soča River", + "description": "Experience the thrill of navigating the turquoise waters of the Soča River, surrounded by breathtaking alpine scenery. Choose from various difficulty levels and enjoy a guided rafting tour that combines adventure and the beauty of nature. This activity is perfect for adventure seekers and nature lovers.", + "locationName": "Soča Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "slovenia", + "ref": "whitewater-rafting-on-the-so-a-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_whitewater-rafting-on-the-so-a-river.jpg" + }, + { + "name": "Predjama Castle Exploration", + "description": "Step into a fairytale at Predjama Castle, a Renaissance castle built within a cave mouth. Explore the intriguing chambers and secret tunnels, and learn about the legend of the robber baron Erasmus. This unique castle offers a fascinating glimpse into Slovenia's history and architecture.", + "locationName": "Predjama", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "slovenia", + "ref": "predjama-castle-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_predjama-castle-exploration.jpg" + }, + { + "name": "Škocjan Caves Tour", + "description": "Descend into the UNESCO-listed Škocjan Caves, a natural wonder with massive underground chambers, towering stalagmites and stalactites, and an underground river. Take a guided tour to witness the awe-inspiring beauty of this subterranean world.", + "locationName": "Škocjan Caves Regional Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "slovenia", + "ref": "-kocjan-caves-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_-kocjan-caves-tour.jpg" + }, + { + "name": "Lipica Stud Farm Visit", + "description": "Discover the home of the Lipizzaner horses at the Lipica Stud Farm. Take a tour to learn about the history and breeding of these elegant white horses, watch a training session, or even enjoy a carriage ride through the picturesque estate.", + "locationName": "Lipica", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "slovenia", + "ref": "lipica-stud-farm-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_lipica-stud-farm-visit.jpg" + }, + { + "name": "Piran Coastal Town Charm", + "description": "Wander through the charming coastal town of Piran, with its Venetian-inspired architecture, narrow streets, and picturesque harbor. Explore the historic Tartini Square, climb the bell tower for panoramic views, and enjoy fresh seafood at a local restaurant.", + "locationName": "Piran", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "slovenia", + "ref": "piran-coastal-town-charm", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_piran-coastal-town-charm.jpg" + }, + { + "name": "Cycling Through the Vipava Valley", + "description": "Embark on a scenic cycling journey through the picturesque Vipava Valley, known for its rolling vineyards, charming villages, and delicious local cuisine. Rent a bike and explore the numerous cycling trails, stopping at family-run wineries for tastings, enjoying farm-to-table meals, and soaking in the stunning landscapes.", + "locationName": "Vipava Valley", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "slovenia", + "ref": "cycling-through-the-vipava-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_cycling-through-the-vipava-valley.jpg" + }, + { + "name": "Kayaking on Lake Bohinj", + "description": "Escape the crowds of Lake Bled and discover the tranquility of Lake Bohinj, Slovenia's largest glacial lake. Rent a kayak and paddle across the crystal-clear waters, surrounded by breathtaking mountain scenery. Enjoy a picnic on the shore, take a refreshing swim, or simply relax and soak in the peaceful atmosphere.", + "locationName": "Lake Bohinj", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "slovenia", + "ref": "kayaking-on-lake-bohinj", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_kayaking-on-lake-bohinj.jpg" + }, + { + "name": "Ziplining in the Ukanc Valley", + "description": "Experience an adrenaline-pumping adventure ziplining through the Ukanc Valley. Soar above the treetops and enjoy breathtaking views of the surrounding mountains and forests. This thrilling activity is perfect for adventure seekers and nature enthusiasts.", + "locationName": "Ukanc Valley", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "slovenia", + "ref": "ziplining-in-the-ukanc-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_ziplining-in-the-ukanc-valley.jpg" + }, + { + "name": "Exploring the Škocjan Caves", + "description": "Embark on a subterranean adventure through the Škocjan Caves, a UNESCO World Heritage site renowned for its vast chambers, underground canyons, and impressive stalactites and stalagmites. Join a guided tour and marvel at the natural wonders of this unique cave system.", + "locationName": "Škocjan Caves", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "slovenia", + "ref": "exploring-the-kocjan-caves", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_exploring-the-kocjan-caves.jpg" + }, + { + "name": "Soaking in the Thermal Waters of Terme Čatež", + "description": "Indulge in relaxation and rejuvenation at Terme Čatež, one of Slovenia's largest and most popular thermal spa resorts. Enjoy the healing properties of the thermal waters, experience a variety of wellness treatments, and unwind in the numerous pools and saunas.", + "locationName": "Terme Čatež", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "slovenia", + "ref": "soaking-in-the-thermal-waters-of-terme-ate-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_soaking-in-the-thermal-waters-of-terme-ate-.jpg" + }, + { + "name": "Stand Up Paddleboarding on Lake Bohinj", + "description": "Experience the tranquility of Lake Bohinj, a glacial lake nestled amidst the Julian Alps, from a unique perspective. Glide across the crystal-clear waters on a stand-up paddleboard, surrounded by breathtaking mountain scenery. This activity is suitable for all skill levels, offering a peaceful and relaxing way to connect with nature.", + "locationName": "Lake Bohinj", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "slovenia", + "ref": "stand-up-paddleboarding-on-lake-bohinj", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_stand-up-paddleboarding-on-lake-bohinj.jpg" + }, + { + "name": "Culinary Tour of Ljubljana", + "description": "Embark on a delectable journey through the culinary scene of Ljubljana, the charming capital city. Join a guided food tour to savor traditional Slovenian dishes, from hearty stews to delicate pastries. Discover local markets, hidden gems, and renowned restaurants, learning about the country's rich gastronomic heritage and indulging in authentic flavors.", + "locationName": "Ljubljana", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "slovenia", + "ref": "culinary-tour-of-ljubljana", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_culinary-tour-of-ljubljana.jpg" + }, + { + "name": "Visit the Velika Planina Alpine Pasture", + "description": "Step back in time at Velika Planina, a traditional alpine pasture nestled in the Kamnik Alps. Take a cable car ride up the mountain and explore the unique herdsmen's settlement with its charming wooden huts. Hike through the picturesque meadows, enjoy breathtaking panoramic views, and sample local dairy products like the famous Trnič cheese.", + "locationName": "Velika Planina", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "slovenia", + "ref": "visit-the-velika-planina-alpine-pasture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_visit-the-velika-planina-alpine-pasture.jpg" + }, + { + "name": "Explore the Škocjan Caves Park", + "description": "Venture into the depths of the Škocjan Caves Park, a UNESCO World Heritage Site renowned for its dramatic underground landscapes. Embark on a guided tour through the vast caverns, marvel at the impressive stalactites and stalagmites, and witness the roaring Reka River flowing through the canyon. This awe-inspiring adventure offers a glimpse into the geological wonders of Slovenia.", + "locationName": "Škocjan Caves Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "slovenia", + "ref": "explore-the-kocjan-caves-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_explore-the-kocjan-caves-park.jpg" + }, + { + "name": "Glamping in the Slovenian Countryside", + "description": "Escape the hustle and bustle of city life and immerse yourself in the tranquility of the Slovenian countryside. Enjoy a unique glamping experience, staying in luxurious tents or eco-friendly cabins surrounded by nature. Wake up to stunning views, explore the nearby forests and meadows, and unwind under the starry sky.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "slovenia", + "ref": "glamping-in-the-slovenian-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/slovenia_glamping-in-the-slovenian-countryside.jpg" + }, + { + "name": "Explore the Ancient City of Sigiriya", + "description": "Embark on a journey to the top of Sigiriya, a UNESCO World Heritage Site. Climb the ancient rock fortress, marvel at the stunning frescoes, and enjoy breathtaking panoramic views of the surrounding landscapes. Discover the fascinating history and legends of this iconic landmark, dating back to the 5th century.", + "locationName": "Sigiriya", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "sri-lanka", + "ref": "explore-the-ancient-city-of-sigiriya", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_explore-the-ancient-city-of-sigiriya.jpg" + }, + { + "name": "Relax on the Beaches of Bentota", + "description": "Escape to the golden shores of Bentota, a renowned beach destination on the southwestern coast. Soak up the sun, swim in the turquoise waters, and indulge in water sports like surfing, snorkeling, or jet skiing. Enjoy the serene ambiance and unwind in a beachfront resort or a cozy beach shack.", + "locationName": "Bentota", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "relax-on-the-beaches-of-bentota", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_relax-on-the-beaches-of-bentota.jpg" + }, + { + "name": "Go on a Wildlife Safari in Yala National Park", + "description": "Embark on an unforgettable wildlife adventure in Yala National Park, renowned for its diverse wildlife population. Spot elephants, leopards, sloth bears, crocodiles, and numerous bird species as you traverse through the park's varied landscapes. Experience the thrill of observing these magnificent creatures in their natural habitat.", + "locationName": "Yala National Park", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "sri-lanka", + "ref": "go-on-a-wildlife-safari-in-yala-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_go-on-a-wildlife-safari-in-yala-national-park.jpg" + }, + { + "name": "Discover the Cultural Triangle", + "description": "Immerse yourself in the rich history and culture of Sri Lanka by exploring the Cultural Triangle. Visit ancient cities like Anuradhapura and Polonnaruwa, home to magnificent ruins, temples, and palaces. Discover the sacred Temple of the Tooth in Kandy, a revered Buddhist pilgrimage site, and witness traditional Kandyan dance performances.", + "locationName": "Cultural Triangle (Anuradhapura, Polonnaruwa, Kandy)", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "sri-lanka", + "ref": "discover-the-cultural-triangle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_discover-the-cultural-triangle.jpg" + }, + { + "name": "Experience the Hill Country Charm in Nuwara Eliya", + "description": "Escape to the cool climate and scenic beauty of Nuwara Eliya, known as 'Little England'. Explore the lush tea plantations, visit a tea factory to learn about the tea-making process, and enjoy a cup of freshly brewed Ceylon tea. Take a scenic train ride through the rolling hills, visit waterfalls, and admire the colonial architecture.", + "locationName": "Nuwara Eliya", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "experience-the-hill-country-charm-in-nuwara-eliya", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_experience-the-hill-country-charm-in-nuwara-eliya.jpg" + }, + { + "name": "Dive into the Underwater World", + "description": "Explore the vibrant coral reefs and diverse marine life of Sri Lanka's coastline. Popular diving spots include Hikkaduwa, Unawatuna, and Trincomalee, where you can encounter colorful fish, sea turtles, and even shipwrecks. Whether you're a beginner or an experienced diver, there are options for all levels.", + "locationName": "Hikkaduwa, Unawatuna, or Trincomalee", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "sri-lanka", + "ref": "dive-into-the-underwater-world", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_dive-into-the-underwater-world.jpg" + }, + { + "name": "Ride the Waves in Arugam Bay", + "description": "Catch some waves at Arugam Bay, a renowned surfing destination on the east coast. With consistent swells and a laid-back atmosphere, it's a paradise for surfers of all skill levels. Take a lesson, rent a board, and experience the thrill of riding the Indian Ocean.", + "locationName": "Arugam Bay", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "ride-the-waves-in-arugam-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_ride-the-waves-in-arugam-bay.jpg" + }, + { + "name": "Hike to Adam's Peak", + "description": "Embark on a pilgrimage to Adam's Peak, a sacred mountain with stunning views. The climb is challenging but rewarding, especially at sunrise when you can witness the breathtaking 'Sri Pada' or sacred footprint. This is a cultural and spiritual experience not to be missed.", + "locationName": "Adam's Peak", + "duration": 6, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "hike-to-adam-s-peak", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_hike-to-adam-s-peak.jpg" + }, + { + "name": "Cruise Along the Dutch Canal in Negombo", + "description": "Take a relaxing boat ride along the historic Dutch Canal in Negombo. Admire the colonial architecture, observe local life, and enjoy the scenic beauty of the waterways. This is a perfect way to unwind and experience a different side of Sri Lanka.", + "locationName": "Negombo", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "cruise-along-the-dutch-canal-in-negombo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_cruise-along-the-dutch-canal-in-negombo.jpg" + }, + { + "name": "Wander Through the Royal Botanical Gardens", + "description": "Escape the hustle and bustle in the serene Royal Botanical Gardens in Peradeniya. Explore diverse plant collections, including orchids, spices, and medicinal herbs. Enjoy a peaceful stroll, have a picnic, and immerse yourself in the beauty of nature.", + "locationName": "Peradeniya", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "sri-lanka", + "ref": "wander-through-the-royal-botanical-gardens", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_wander-through-the-royal-botanical-gardens.jpg" + }, + { + "name": "Whale Watching in Mirissa", + "description": "Embark on a thrilling boat excursion from the coastal town of Mirissa to witness the majestic spectacle of whales in their natural habitat. Sri Lanka is renowned as one of the best places in the world to spot blue whales, sperm whales, and dolphins. Keep your eyes peeled for these gentle giants as they breach the surface and marvel at their sheer size and grace.", + "locationName": "Mirissa", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "sri-lanka", + "ref": "whale-watching-in-mirissa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_whale-watching-in-mirissa.jpg" + }, + { + "name": "Explore the Galle Dutch Fort", + "description": "Step back in time with a visit to the historic Galle Dutch Fort, a UNESCO World Heritage Site. Wander through the charming streets lined with Dutch colonial buildings, explore the ramparts offering stunning ocean views, and discover quaint shops, cafes, and museums. Immerse yourself in the rich history and architectural beauty of this captivating landmark.", + "locationName": "Galle", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "explore-the-galle-dutch-fort", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_explore-the-galle-dutch-fort.jpg" + }, + { + "name": "Visit a Tea Plantation and Factory", + "description": "Delve into the world of Ceylon tea with a visit to a scenic tea plantation and factory in the Hill Country. Learn about the tea-making process, from plucking the leaves to the final stages of production. Enjoy a cup of freshly brewed tea while taking in the breathtaking views of rolling hills carpeted with emerald-green tea bushes.", + "locationName": "Nuwara Eliya", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "visit-a-tea-plantation-and-factory", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_visit-a-tea-plantation-and-factory.jpg" + }, + { + "name": "Learn to Cook Authentic Sri Lankan Cuisine", + "description": "Embark on a culinary adventure by participating in a cooking class and learn the secrets of Sri Lankan cuisine. Master the art of preparing traditional dishes such as rice and curry, coconut roti, and flavorful sambals. Discover the unique blend of spices and flavors that make Sri Lankan food so special and enjoy a delicious meal you've created yourself.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "sri-lanka", + "ref": "learn-to-cook-authentic-sri-lankan-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_learn-to-cook-authentic-sri-lankan-cuisine.jpg" + }, + { + "name": "Experience a Traditional Ayurvedic Treatment", + "description": "Indulge in a rejuvenating Ayurvedic treatment and experience the ancient healing traditions of Sri Lanka. Choose from a variety of therapies, including massages, herbal baths, and oil treatments, designed to restore balance and promote well-being. Immerse yourself in a sanctuary of relaxation and emerge feeling refreshed and revitalized.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "sri-lanka", + "ref": "experience-a-traditional-ayurvedic-treatment", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_experience-a-traditional-ayurvedic-treatment.jpg" + }, + { + "name": "White Water Rafting in Kitulgala", + "description": "Embark on an exhilarating white water rafting adventure down the Kelani River in Kitulgala. Navigate through thrilling rapids surrounded by lush rainforests, offering a perfect blend of adrenaline and stunning natural beauty.", + "locationName": "Kitulgala", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "sri-lanka", + "ref": "white-water-rafting-in-kitulgala", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_white-water-rafting-in-kitulgala.jpg" + }, + { + "name": "Bird Watching in Kumana National Park", + "description": "Discover the rich avian diversity of Sri Lanka at Kumana National Park. Home to a wide variety of resident and migratory birds, including pelicans, painted storks, and spoonbills, this park offers a paradise for bird enthusiasts.", + "locationName": "Kumana National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "bird-watching-in-kumana-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_bird-watching-in-kumana-national-park.jpg" + }, + { + "name": "Explore the Dambulla Cave Temple", + "description": "Step into the spiritual realm at the Dambulla Cave Temple, a UNESCO World Heritage Site. Marvel at the ancient Buddhist murals and statues housed within five cave temples, showcasing Sri Lanka's rich history and religious significance.", + "locationName": "Dambulla", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "explore-the-dambulla-cave-temple", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_explore-the-dambulla-cave-temple.jpg" + }, + { + "name": "Take a Scenic Train Journey from Kandy to Ella", + "description": "Embark on a breathtaking train journey through Sri Lanka's scenic hill country. Travel from Kandy to Ella, passing through rolling hills, tea plantations, and picturesque waterfalls, offering unforgettable views of the island's natural beauty.", + "locationName": "Kandy to Ella", + "duration": 7, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "sri-lanka", + "ref": "take-a-scenic-train-journey-from-kandy-to-ella", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_take-a-scenic-train-journey-from-kandy-to-ella.jpg" + }, + { + "name": "Visit the Pinnawala Elephant Orphanage", + "description": "Witness the heartwarming sight of rescued elephants at the Pinnawala Elephant Orphanage. Observe these gentle giants as they bathe, feed, and interact with each other, providing a unique opportunity to learn about elephant conservation efforts.", + "locationName": "Pinnawala", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "sri-lanka", + "ref": "visit-the-pinnawala-elephant-orphanage", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/sri-lanka_visit-the-pinnawala-elephant-orphanage.jpg" + }, + { + "name": "Polar Bear Safari", + "description": "Embark on an unforgettable expedition into the Arctic wilderness in search of the majestic polar bear. Accompanied by experienced guides, you'll navigate the icy landscapes by snowmobile or boat, keeping a keen eye out for these incredible creatures in their natural habitat. Witnessing these powerful predators in action is an experience that will stay with you forever.", + "locationName": "Various locations depending on the season and ice conditions", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "svalbard", + "ref": "polar-bear-safari", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_polar-bear-safari.jpg" + }, + { + "name": "Dog Sledding Adventure", + "description": "Experience the thrill of gliding across the snowy plains of Svalbard on a traditional dog sled. Learn the art of mushing from expert guides as you command your team of huskies through breathtaking Arctic scenery. Feel the adrenaline rush as you race across the frozen tundra, surrounded by the pristine beauty of the polar landscape.", + "locationName": "Various locations around Longyearbyen", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "svalbard", + "ref": "dog-sledding-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_dog-sledding-adventure.jpg" + }, + { + "name": "Northern Lights Viewing", + "description": "Venture out into the darkness of the Arctic night and witness the awe-inspiring spectacle of the Northern Lights. Svalbard's remote location and minimal light pollution offer ideal conditions for observing this celestial phenomenon. Gaze up at the sky as vibrant ribbons of green, purple, and pink dance across the horizon, creating an unforgettable display of nature's magic.", + "locationName": "Various locations away from city lights", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "svalbard", + "ref": "northern-lights-viewing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_northern-lights-viewing.jpg" + }, + { + "name": "Kayaking in the Fjords", + "description": "Paddle through the tranquil waters of Svalbard's fjords and immerse yourself in the serene beauty of the Arctic landscape. Glide past towering glaciers, rugged mountains, and pristine icebergs as you explore hidden coves and observe the diverse marine life. Kayaking offers a unique and peaceful perspective on the Arctic wilderness.", + "locationName": "Isfjorden or other suitable fjords", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "svalbard", + "ref": "kayaking-in-the-fjords", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_kayaking-in-the-fjords.jpg" + }, + { + "name": "Visit the Svalbard Museum", + "description": "Delve into the rich history and unique culture of Svalbard at the Svalbard Museum. Explore exhibits showcasing the archipelago's fascinating past, from early exploration and whaling to coal mining and scientific research. Learn about the challenges and triumphs of human life in this remote Arctic region, and gain a deeper understanding of the delicate balance between nature and human activity.", + "locationName": "Longyearbyen", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "svalbard", + "ref": "visit-the-svalbard-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_visit-the-svalbard-museum.jpg" + }, + { + "name": "Snowmobiling across the Tundra", + "description": "Embark on an exhilarating snowmobile adventure across the vast Arctic tundra. Feel the rush of adrenaline as you zoom through the snowy landscapes, taking in the breathtaking scenery and the crisp Arctic air. This thrilling experience offers a unique way to explore the remote wilderness of Svalbard.", + "locationName": "Various locations across Svalbard", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "svalbard", + "ref": "snowmobiling-across-the-tundra", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_snowmobiling-across-the-tundra.jpg" + }, + { + "name": "Hiking to a Glacier", + "description": "Lace up your hiking boots and embark on an unforgettable trek to one of Svalbard's majestic glaciers. Witness the immense power and beauty of these icy giants up close as you traverse the Arctic terrain. Experienced guides will lead you on a safe and informative journey, sharing insights about the region's geology and glacial formations.", + "locationName": "Various glaciers across Svalbard (e.g., Longyearbreen, Esmarkbreen)", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "svalbard", + "ref": "hiking-to-a-glacier", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_hiking-to-a-glacier.jpg" + }, + { + "name": "Boat Trip to Pyramiden", + "description": "Take a scenic boat trip to the abandoned Russian mining settlement of Pyramiden. Explore the ghost town's eerie remains, including its once-grand cultural center, sports complex, and residential buildings. Learn about the fascinating history of this Soviet-era outpost and its sudden abandonment in the 1990s.", + "locationName": "Pyramiden", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "svalbard", + "ref": "boat-trip-to-pyramiden", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_boat-trip-to-pyramiden.jpg" + }, + { + "name": "Visit the North Pole Expedition Museum", + "description": "Delve into the rich history of Arctic exploration at the North Pole Expedition Museum in Longyearbyen. Discover captivating exhibits showcasing artifacts, photographs, and stories from famous expeditions, including those of Roald Amundsen and Fridtjof Nansen. Gain insights into the challenges and triumphs of polar exploration and its impact on our understanding of the Arctic.", + "locationName": "Longyearbyen", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "svalbard", + "ref": "visit-the-north-pole-expedition-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_visit-the-north-pole-expedition-museum.jpg" + }, + { + "name": "Enjoy Local Cuisine and Culture", + "description": "Indulge in the unique flavors of Svalbard's local cuisine. Sample Arctic delicacies like reindeer steak, fresh seafood, and cloudberries. Visit cozy cafes and restaurants in Longyearbyen, where you can savor traditional dishes and immerse yourself in the local culture. Don't miss the opportunity to try Svalbard's own craft beer brewed with glacial water.", + "locationName": "Longyearbyen", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "svalbard", + "ref": "enjoy-local-cuisine-and-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_enjoy-local-cuisine-and-culture.jpg" + }, + { + "name": "Fossil Hunting at Mine 7", + "description": "Embark on a unique adventure to Mine 7, a former coal mine near Longyearbyen. Here, you can explore the remnants of Svalbard's mining history and participate in guided fossil hunting tours. Uncover ancient plant fossils dating back millions of years and learn about the geological forces that shaped this Arctic landscape. This activity offers a fascinating glimpse into Svalbard's past and is suitable for all ages.", + "locationName": "Mine 7", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "svalbard", + "ref": "fossil-hunting-at-mine-7", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_fossil-hunting-at-mine-7.jpg" + }, + { + "name": "Birdwatching at the Alkefjellet Bird Cliffs", + "description": "Witness the spectacle of thousands of seabirds nesting on the dramatic cliffs of Alkefjellet. Take a boat trip to this impressive location and marvel at the guillemots, kittiwakes, and Brünnich's guillemots that call these cliffs home. Capture stunning photographs of the birds in flight and observe their nesting behaviors. This experience is a must for bird enthusiasts and nature lovers.", + "locationName": "Alkefjellet Bird Cliffs", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "svalbard", + "ref": "birdwatching-at-the-alkefjellet-bird-cliffs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_birdwatching-at-the-alkefjellet-bird-cliffs.jpg" + }, + { + "name": "Ice Caving Adventure", + "description": "Delve into the mesmerizing world of ice caves beneath Svalbard's glaciers. Join a guided tour and explore these natural ice formations, adorned with sparkling ice crystals and unique ice sculptures. Learn about the formation of glaciers and ice caves while experiencing the thrill of venturing underground in this Arctic wonderland. This activity is ideal for adventurous travelers seeking an unforgettable experience.", + "locationName": "Various glacier locations", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "svalbard", + "ref": "ice-caving-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_ice-caving-adventure.jpg" + }, + { + "name": "Relaxation at the Svalbard Seed Vault", + "description": "Visit the iconic Svalbard Global Seed Vault, a secure facility built to safeguard the world's crop diversity. While entry inside the vault is restricted, you can admire its unique architecture and learn about its crucial role in preserving biodiversity. The surrounding area offers stunning views of the Arctic landscape, providing a peaceful setting for reflection and appreciation of nature's resilience.", + "locationName": "Svalbard Global Seed Vault", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "svalbard", + "ref": "relaxation-at-the-svalbard-seed-vault", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_relaxation-at-the-svalbard-seed-vault.jpg" + }, + { + "name": "Stargazing in the Arctic Wilderness", + "description": "Experience the magic of the Arctic night sky away from light pollution. Join a guided stargazing tour and marvel at the celestial wonders above. Learn about constellations, planets, and the Aurora Borealis, also known as the Northern Lights, which illuminate the sky with vibrant colors. This activity offers a unique opportunity to connect with the vastness of the universe and appreciate the beauty of the Arctic night.", + "locationName": "Various locations away from settlements", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "svalbard", + "ref": "stargazing-in-the-arctic-wilderness", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_stargazing-in-the-arctic-wilderness.jpg" + }, + { + "name": "Arctic Photography Expedition", + "description": "Embark on a guided photography tour to capture the stunning landscapes of Svalbard. Learn from professional photographers and get tips on capturing the Arctic's unique light and wildlife. This activity is perfect for both amateur and experienced photographers looking to create lasting memories.", + "locationName": "Various locations throughout Svalbard", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "svalbard", + "ref": "arctic-photography-expedition", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_arctic-photography-expedition.jpg" + }, + { + "name": "Visit Barentsburg, a Russian Mining Town", + "description": "Take a boat trip to Barentsburg, a Russian coal-mining settlement on Spitsbergen. Experience a different culture and explore the town's unique architecture, history, and way of life. You can even enjoy a traditional Russian meal at a local restaurant.", + "locationName": "Barentsburg", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "svalbard", + "ref": "visit-barentsburg-a-russian-mining-town", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_visit-barentsburg-a-russian-mining-town.jpg" + }, + { + "name": "Go Cross-Country Skiing", + "description": "Explore the Arctic wilderness on skis with a cross-country skiing excursion. Glide across snow-covered landscapes, taking in the breathtaking scenery and enjoying the fresh air and exercise. This activity is suitable for various skill levels, and guided tours are available for beginners.", + "locationName": "Various locations throughout Svalbard", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "svalbard", + "ref": "go-cross-country-skiing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_go-cross-country-skiing.jpg" + }, + { + "name": "Take a Hot Air Balloon Ride over Svalbard", + "description": "Experience the ultimate Arctic adventure with a hot air balloon ride over the stunning landscapes of Svalbard. Enjoy breathtaking aerial views of glaciers, mountains, and fjords, and witness the Arctic wilderness from a unique perspective. This once-in-a-lifetime experience is perfect for special occasions or for those seeking an unforgettable adventure.", + "locationName": "Various locations throughout Svalbard", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "svalbard", + "ref": "take-a-hot-air-balloon-ride-over-svalbard", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_take-a-hot-air-balloon-ride-over-svalbard.jpg" + }, + { + "name": "Enjoy the Local Arts and Culture Scene", + "description": "Explore the local arts and culture scene in Longyearbyen, the largest settlement in Svalbard. Visit art galleries showcasing Arctic-inspired works, attend cultural events, and learn about the unique history and traditions of this remote community.", + "locationName": "Longyearbyen", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "svalbard", + "ref": "enjoy-the-local-arts-and-culture-scene", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/svalbard_enjoy-the-local-arts-and-culture-scene.jpg" + }, + { + "name": "Hiking in the Jungfrau Region", + "description": "Embark on an unforgettable hiking adventure amidst the stunning landscapes of the Jungfrau Region. Explore well-maintained trails that wind through alpine meadows, past glacial lakes, and offer breathtaking panoramic views of snow-capped peaks. Whether you're a seasoned hiker or a beginner, there are trails suitable for all levels of experience. Don't miss the iconic trails like the Panorama Trail from First to Grindelwald or the Eiger Trail for a challenging yet rewarding hike.", + "locationName": "Jungfrau Region", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "swiss-alps", + "ref": "hiking-in-the-jungfrau-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_hiking-in-the-jungfrau-region.jpg" + }, + { + "name": "Skiing in Zermatt", + "description": "Experience world-class skiing in the renowned resort town of Zermatt. With over 360 kilometers of slopes, Zermatt offers something for every skier, from gentle beginner runs to challenging off-piste terrain. Enjoy breathtaking views of the iconic Matterhorn while gliding down the slopes. After a day on the mountain, indulge in the après-ski scene at one of the many cozy restaurants or bars.", + "locationName": "Zermatt", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "swiss-alps", + "ref": "skiing-in-zermatt", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_skiing-in-zermatt.jpg" + }, + { + "name": "Scenic Train Ride on the Glacier Express", + "description": "Embark on a breathtaking journey through the Swiss Alps on the Glacier Express, known as the 'slowest express train in the world.' Sit back and relax as you travel through stunning mountain scenery, passing over bridges, through tunnels, and alongside glaciers. The panoramic windows offer unparalleled views of the alpine landscape, making it a truly unforgettable experience.", + "locationName": "Glacier Express Route", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "swiss-alps", + "ref": "scenic-train-ride-on-the-glacier-express", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_scenic-train-ride-on-the-glacier-express.jpg" + }, + { + "name": "Paragliding Over Interlaken", + "description": "Soar through the skies and experience the Swiss Alps from a unique perspective with a paragliding adventure over Interlaken. Take off from a mountaintop and enjoy breathtaking panoramic views of the surrounding mountains, lakes, and valleys. Feel the thrill of flying as you glide through the air, accompanied by experienced pilots who ensure your safety and enjoyment.", + "locationName": "Interlaken", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 4, + "destinationRef": "swiss-alps", + "ref": "paragliding-over-interlaken", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_paragliding-over-interlaken.jpg" + }, + { + "name": "Visit Charming Villages", + "description": "Explore the charming villages nestled amidst the Swiss Alps, each with its unique character and beauty. Wander through cobbled streets, admire traditional architecture, and discover local shops and restaurants. Visit Grindelwald, a popular village with stunning views of the Eiger, or Zermatt, a car-free village at the foot of the Matterhorn. Immerse yourself in the local culture and enjoy the peaceful atmosphere of these alpine gems.", + "locationName": "Various villages", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "swiss-alps", + "ref": "visit-charming-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_visit-charming-villages.jpg" + }, + { + "name": "Mountain Biking", + "description": "Embark on a thrilling mountain biking adventure through the stunning Swiss Alps. Explore diverse terrains, from gentle slopes to challenging trails, and enjoy breathtaking views of snow-capped peaks, lush valleys, and picturesque villages. Whether you're a seasoned rider or a beginner, there are trails suitable for all skill levels, offering an exhilarating and unforgettable experience.", + "locationName": "Various trails throughout the Swiss Alps", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "swiss-alps", + "ref": "mountain-biking", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_mountain-biking.jpg" + }, + { + "name": "Indulge in Swiss Chocolate Delights", + "description": "Embark on a delectable journey into the world of Swiss chocolate. Visit renowned chocolate factories and artisanal shops, where you can witness the art of chocolate making, learn about its rich history, and savor a wide variety of exquisite chocolates, from creamy milk to intense dark. Don't miss the opportunity to create your own chocolate masterpiece during a fun and interactive workshop.", + "locationName": "Chocolate factories and shops in Swiss towns and cities", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "swiss-alps", + "ref": "indulge-in-swiss-chocolate-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_indulge-in-swiss-chocolate-delights.jpg" + }, + { + "name": "Experience Thrilling Water Sports on Lake Geneva", + "description": "Head to the picturesque Lake Geneva and dive into a world of exciting water sports. Try your hand at windsurfing, sailing, or kayaking, enjoying the refreshing breeze and stunning views of the surrounding mountains. For an adrenaline rush, opt for water skiing or wakeboarding, gliding across the crystal-clear waters. Whether you're seeking adventure or relaxation, Lake Geneva offers endless possibilities.", + "locationName": "Lake Geneva", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "swiss-alps", + "ref": "experience-thrilling-water-sports-on-lake-geneva", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_experience-thrilling-water-sports-on-lake-geneva.jpg" + }, + { + "name": "Unwind in Thermal Baths and Spa Resorts", + "description": "Escape to the tranquil oasis of Swiss thermal baths and spa resorts, renowned for their rejuvenating properties. Immerse yourself in warm, mineral-rich waters, surrounded by breathtaking alpine scenery. Indulge in a variety of wellness treatments, including massages, facials, and body wraps, leaving you feeling refreshed and revitalized.", + "locationName": "Various spa resorts throughout the Swiss Alps", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "swiss-alps", + "ref": "unwind-in-thermal-baths-and-spa-resorts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_unwind-in-thermal-baths-and-spa-resorts.jpg" + }, + { + "name": "Discover Swiss Culture and Heritage", + "description": "Immerse yourself in the rich culture and heritage of Switzerland by visiting charming towns and cities. Explore historical landmarks, museums, and art galleries, learning about the country's fascinating past and vibrant present. Attend traditional festivals and events, experiencing authentic Swiss folklore, music, and dance. Don't forget to sample local delicacies and wines, savoring the unique flavors of the region.", + "locationName": "Swiss towns and cities", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "swiss-alps", + "ref": "discover-swiss-culture-and-heritage", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_discover-swiss-culture-and-heritage.jpg" + }, + { + "name": "Canyoning in Interlaken", + "description": "Embark on an exhilarating canyoning adventure in Interlaken, where you'll rappel down cascading waterfalls, slide down natural rock formations, and jump into crystal-clear pools. This adrenaline-pumping activity offers a unique way to experience the stunning gorges and canyons of the Swiss Alps.", + "locationName": "Interlaken", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "swiss-alps", + "ref": "canyoning-in-interlaken", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_canyoning-in-interlaken.jpg" + }, + { + "name": "Visit the Chillon Castle", + "description": "Step back in time with a visit to the iconic Chillon Castle, a medieval fortress located on the shores of Lake Geneva. Explore its ancient halls, dungeons, and courtyards, and learn about its fascinating history dating back over a thousand years. Enjoy breathtaking views of the lake and surrounding mountains.", + "locationName": "Montreux", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "swiss-alps", + "ref": "visit-the-chillon-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_visit-the-chillon-castle.jpg" + }, + { + "name": "Stargazing in the Mountains", + "description": "Escape the city lights and experience the magic of stargazing in the pristine Swiss Alps. Join a guided tour or find a secluded spot to marvel at the Milky Way and constellations. The clear mountain air and high altitude provide exceptional conditions for observing the night sky.", + "locationName": "Jungfrau Region", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "swiss-alps", + "ref": "stargazing-in-the-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_stargazing-in-the-mountains.jpg" + }, + { + "name": "Hot Air Balloon Ride over the Alps", + "description": "Soar above the majestic Swiss Alps in a hot air balloon and witness breathtaking panoramic views of snow-capped peaks, valleys, and glaciers. Enjoy a peaceful and unforgettable experience as you drift through the sky and capture stunning aerial photos.", + "locationName": "Château-d'Oex", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "swiss-alps", + "ref": "hot-air-balloon-ride-over-the-alps", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_hot-air-balloon-ride-over-the-alps.jpg" + }, + { + "name": "Cheese Fondue and Wine Tasting", + "description": "Indulge in the quintessential Swiss experience of cheese fondue and wine tasting. Visit a traditional restaurant or chalet and savor the rich flavors of melted cheese accompanied by local wines. Learn about the art of fondue making and the different varieties of cheese and wine produced in the region.", + "locationName": "Gruyères", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "swiss-alps", + "ref": "cheese-fondue-and-wine-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_cheese-fondue-and-wine-tasting.jpg" + }, + { + "name": "Ice Climbing on a Glacier", + "description": "Embark on a thrilling ice climbing adventure on one of Switzerland's majestic glaciers. Experienced guides will lead you through the basics of ice axe and crampon use, ensuring your safety as you ascend glistening ice walls. This exhilarating activity offers a unique perspective of the alpine landscape and an unforgettable adrenaline rush.", + "locationName": "Aletsch Glacier or Rhône Glacier", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "swiss-alps", + "ref": "ice-climbing-on-a-glacier", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_ice-climbing-on-a-glacier.jpg" + }, + { + "name": "Wildlife Watching in the Swiss National Park", + "description": "Immerse yourself in the pristine wilderness of the Swiss National Park, a haven for diverse wildlife. Hike through scenic trails and observe animals in their natural habitat, including ibex, chamois, marmots, and golden eagles. Keep an eye out for rare sightings like the elusive lynx or bearded vulture. This experience is perfect for nature enthusiasts and photographers.", + "locationName": "Swiss National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "swiss-alps", + "ref": "wildlife-watching-in-the-swiss-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_wildlife-watching-in-the-swiss-national-park.jpg" + }, + { + "name": "Explore the Charming Town of Gruyères", + "description": "Step back in time and visit the medieval town of Gruyères, renowned for its namesake cheese. Wander through cobbled streets, admire the historic architecture, and visit the Gruyères Castle. Indulge in a cheese fondue tasting and explore the Maison du Gruyère to learn about the cheese-making process. Don't miss the HR Giger Museum, showcasing the works of the Swiss surrealist artist.", + "locationName": "Gruyères", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "swiss-alps", + "ref": "explore-the-charming-town-of-gruy-res", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_explore-the-charming-town-of-gruy-res.jpg" + }, + { + "name": "Take a Boat Ride on Lake Lucerne", + "description": "Enjoy a scenic boat ride on the crystal-clear waters of Lake Lucerne, surrounded by breathtaking mountain vistas. Relax on deck and admire the picturesque towns, rolling hills, and snow-capped peaks. Consider a themed cruise, such as a culinary journey or a historical tour, to enhance your experience.", + "locationName": "Lake Lucerne", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "swiss-alps", + "ref": "take-a-boat-ride-on-lake-lucerne", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_take-a-boat-ride-on-lake-lucerne.jpg" + }, + { + "name": "Visit the Jungfraujoch - Top of Europe", + "description": "Embark on an unforgettable journey to the Jungfraujoch, the highest railway station in Europe, known as the \"Top of Europe\". Marvel at the panoramic views of the surrounding Alps, including the Aletsch Glacier. Explore the Ice Palace, experience the Sphinx Observatory, and enjoy a meal at one of the restaurants with breathtaking views.", + "locationName": "Jungfraujoch", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "swiss-alps", + "ref": "visit-the-jungfraujoch-top-of-europe", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/swiss-alps_visit-the-jungfraujoch-top-of-europe.jpg" + }, + { + "name": "Cradle Mountain National Park Hike", + "description": "Embark on a breathtaking hike through the iconic Cradle Mountain National Park. Witness the rugged beauty of Cradle Mountain, Dove Lake, and surrounding peaks. Keep an eye out for unique wildlife like wombats, wallabies, and Tasmanian devils.", + "locationName": "Cradle Mountain National Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "tasmania", + "ref": "cradle-mountain-national-park-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_cradle-mountain-national-park-hike.jpg" + }, + { + "name": "Wineglass Bay Cruise", + "description": "Sail across the turquoise waters of Wineglass Bay, marveling at the pristine beaches and dramatic cliffs. Enjoy swimming, snorkeling, or simply relaxing on the deck while soaking up the stunning coastal scenery.", + "locationName": "Wineglass Bay", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "tasmania", + "ref": "wineglass-bay-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_wineglass-bay-cruise.jpg" + }, + { + "name": "Port Arthur Historic Site Tour", + "description": "Step back in time at the Port Arthur Historic Site, a former penal colony with a fascinating and sometimes haunting history. Explore the preserved buildings, learn about the lives of convicts, and discover the stories of this UNESCO World Heritage site.", + "locationName": "Port Arthur", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "tasmania", + "ref": "port-arthur-historic-site-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_port-arthur-historic-site-tour.jpg" + }, + { + "name": "Bonorong Wildlife Sanctuary Visit", + "description": "Get up close and personal with Tasmania's unique wildlife at Bonorong Wildlife Sanctuary. See Tasmanian devils, kangaroos, koalas, wombats, and more in their natural habitats. Learn about conservation efforts and the importance of protecting these incredible animals.", + "locationName": "Bonorong Wildlife Sanctuary", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "tasmania", + "ref": "bonorong-wildlife-sanctuary-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_bonorong-wildlife-sanctuary-visit.jpg" + }, + { + "name": "Salamanca Market Exploration", + "description": "Immerse yourself in the vibrant atmosphere of Salamanca Market in Hobart. Browse through stalls offering local arts and crafts, fresh produce, gourmet food, and unique souvenirs. Enjoy live music and entertainment while experiencing the heart of Tasmania's capital city.", + "locationName": "Salamanca Market, Hobart", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "tasmania", + "ref": "salamanca-market-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_salamanca-market-exploration.jpg" + }, + { + "name": "Bruny Island Wilderness Cruise", + "description": "Embark on a thrilling boat tour around Bruny Island, renowned for its rugged coastlines, towering cliffs, and abundant marine life. Witness playful dolphins, curious seals, and majestic seabirds in their natural habitat. Explore sea caves and marvel at the breathtaking scenery, including the iconic Breathing Rock.", + "locationName": "Bruny Island", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "tasmania", + "ref": "bruny-island-wilderness-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_bruny-island-wilderness-cruise.jpg" + }, + { + "name": "Mount Wellington Summit Hike", + "description": "Challenge yourself with a rewarding hike to the summit of Mount Wellington, overlooking the city of Hobart. Enjoy panoramic views of the surrounding landscape, including the Derwent River, Bruny Island, and the Tasman Peninsula. This moderately challenging hike offers a unique perspective of Tasmania's capital and its natural beauty.", + "locationName": "Mount Wellington", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "tasmania", + "ref": "mount-wellington-summit-hike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_mount-wellington-summit-hike.jpg" + }, + { + "name": "Tahune Airwalk and Eagle Glide", + "description": "Experience the rainforest from a new perspective at the Tahune Airwalk. Stroll amongst the treetops on a suspended walkway, offering breathtaking views of the Huon River and surrounding forests. For the ultimate thrill, soar through the air on the Eagle Glide zipline, enjoying an exhilarating journey through the canopy.", + "locationName": "Tahune Forest Airwalk", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "tasmania", + "ref": "tahune-airwalk-and-eagle-glide", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_tahune-airwalk-and-eagle-glide.jpg" + }, + { + "name": "Lavender Farm Tour and Aromatherapy Experience", + "description": "Indulge in the tranquility of a lavender farm, surrounded by the soothing scent of purple blooms. Take a guided tour to learn about lavender cultivation and its uses in aromatherapy and relaxation. Enjoy a workshop to create your own lavender-infused products, taking home a piece of Tasmanian serenity.", + "locationName": "Bridestowe Lavender Estate or other Lavender Farms", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "tasmania", + "ref": "lavender-farm-tour-and-aromatherapy-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_lavender-farm-tour-and-aromatherapy-experience.jpg" + }, + { + "name": "Gordon River Cruise and Heritage Landing", + "description": "Embark on a scenic cruise along the Gordon River, venturing deep into the Tasmanian Wilderness World Heritage Area. Witness the untouched beauty of the rainforest, waterfalls, and gorges. Visit Sarah Island, a former penal colony, and learn about its fascinating history.", + "locationName": "Strahan", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "tasmania", + "ref": "gordon-river-cruise-and-heritage-landing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_gordon-river-cruise-and-heritage-landing.jpg" + }, + { + "name": "Kayaking on the Derwent River", + "description": "Embark on a serene kayaking adventure on the Derwent River, gliding past Hobart's cityscape and enjoying breathtaking views of Mount Wellington. Spot playful dolphins, admire historic landmarks like the Tasman Bridge, and immerse yourself in the tranquility of the surrounding nature. Choose from guided tours or self-guided rentals for a personalized experience.", + "locationName": "Derwent River, Hobart", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tasmania", + "ref": "kayaking-on-the-derwent-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_kayaking-on-the-derwent-river.jpg" + }, + { + "name": "Exploring the Royal Tasmanian Botanical Gardens", + "description": "Wander through the Royal Tasmanian Botanical Gardens, a haven of diverse plant life and serene landscapes. Discover themed gardens like the Japanese Garden and the Subantarctic Plant House, marvel at the vibrant colors of the Conservatory, and enjoy a peaceful picnic amidst the lush greenery. This family-friendly attraction offers something for everyone to appreciate.", + "locationName": "Royal Tasmanian Botanical Gardens, Hobart", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "tasmania", + "ref": "exploring-the-royal-tasmanian-botanical-gardens", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_exploring-the-royal-tasmanian-botanical-gardens.jpg" + }, + { + "name": "Delving into History at the Tasmanian Museum and Art Gallery", + "description": "Immerse yourself in Tasmania's rich history and culture at the Tasmanian Museum and Art Gallery. Explore fascinating exhibits showcasing Aboriginal heritage, colonial artifacts, and contemporary art. Discover the island's unique flora and fauna, learn about its maritime history, and gain insights into the lives of Tasmanian people through the ages.", + "locationName": "Tasmanian Museum and Art Gallery, Hobart", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tasmania", + "ref": "delving-into-history-at-the-tasmanian-museum-and-art-gallery", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_delving-into-history-at-the-tasmanian-museum-and-art-gallery.jpg" + }, + { + "name": "Mountain Biking on the Blue Derby Trails", + "description": "Experience the thrill of mountain biking on the world-renowned Blue Derby Trails. With options for all skill levels, these purpose-built trails wind through stunning forests, offering exhilarating descents, challenging climbs, and breathtaking views. Rent a bike or join a guided tour to explore this mountain biking paradise.", + "locationName": "Blue Derby, North-East Tasmania", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "tasmania", + "ref": "mountain-biking-on-the-blue-derby-trails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_mountain-biking-on-the-blue-derby-trails.jpg" + }, + { + "name": "Stargazing at the Cataract Gorge Reserve", + "description": "Escape the city lights and marvel at the celestial wonders above at the Cataract Gorge Reserve. With minimal light pollution, this natural amphitheater offers spectacular views of the night sky. Join a stargazing tour or simply lay back and enjoy the breathtaking spectacle of stars, constellations, and planets.", + "locationName": "Cataract Gorge Reserve, Launceston", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "tasmania", + "ref": "stargazing-at-the-cataract-gorge-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_stargazing-at-the-cataract-gorge-reserve.jpg" + }, + { + "name": "Scuba Diving in the Tasman Peninsula", + "description": "Embark on an underwater adventure exploring the kelp forests, caves, and shipwrecks around the Tasman Peninsula. Encounter diverse marine life, including seals, seahorses, and colorful fish, in the crystal-clear waters. Several dive operators cater to all experience levels, making it a thrilling experience for beginners and seasoned divers alike.", + "locationName": "Tasman Peninsula", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "tasmania", + "ref": "scuba-diving-in-the-tasman-peninsula", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_scuba-diving-in-the-tasman-peninsula.jpg" + }, + { + "name": "Farm-to-Table Culinary Experience", + "description": "Indulge in Tasmania's renowned gastronomy with a farm-to-table dining experience. Visit local farms and producers, learn about sustainable agricultural practices, and savor the freshest seasonal ingredients transformed into delectable dishes. Many restaurants and wineries offer farm-to-table menus, showcasing the island's culinary bounty.", + "locationName": "Various locations throughout Tasmania", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "tasmania", + "ref": "farm-to-table-culinary-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_farm-to-table-culinary-experience.jpg" + }, + { + "name": "Wildlife Spotting at Maria Island National Park", + "description": "Escape to the car-free haven of Maria Island National Park, known for its abundance of wildlife. Hike or bike through diverse landscapes, encountering wombats, kangaroos, wallabies, and Tasmanian devils in their natural habitat. The island's pristine beaches and historic ruins offer a unique blend of nature and culture.", + "locationName": "Maria Island National Park", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tasmania", + "ref": "wildlife-spotting-at-maria-island-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_wildlife-spotting-at-maria-island-national-park.jpg" + }, + { + "name": "Exploring the Bay of Fires", + "description": "Discover the breathtaking beauty of the Bay of Fires, renowned for its turquoise waters, white sandy beaches, and striking orange-hued granite boulders. Hike along the coast, kayak in the pristine bays, or simply relax on the beach and soak up the scenery. The Bay of Fires offers a perfect blend of relaxation and adventure.", + "locationName": "Bay of Fires", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tasmania", + "ref": "exploring-the-bay-of-fires", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_exploring-the-bay-of-fires.jpg" + }, + { + "name": "Ghost Tour of Port Arthur", + "description": "Delve into the spooky side of Tasmania's history with a ghost tour of Port Arthur Historic Site. Explore the haunting ruins after dark, listening to chilling tales of convicts and paranormal activity. This eerie yet fascinating experience offers a unique perspective on the site's past.", + "locationName": "Port Arthur Historic Site", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "tasmania", + "ref": "ghost-tour-of-port-arthur", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tasmania_ghost-tour-of-port-arthur.jpg" + }, + { + "name": "Relax on the Beach", + "description": "Spend a day soaking up the sun on the beautiful sandy beaches of Tel Aviv. Take a dip in the refreshing Mediterranean Sea, enjoy water sports like surfing or paddleboarding, or simply relax under a beach umbrella with a good book.", + "locationName": "Gordon Beach, Frishman Beach, or Banana Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "tel-aviv", + "ref": "relax-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_relax-on-the-beach.jpg" + }, + { + "name": "Explore the Bauhaus Architecture", + "description": "Tel Aviv is renowned for its collection of Bauhaus buildings, a UNESCO World Heritage Site. Take a walking tour or join a guided excursion to admire the unique architectural style and learn about the history of the White City.", + "locationName": "White City", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "tel-aviv", + "ref": "explore-the-bauhaus-architecture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_explore-the-bauhaus-architecture.jpg" + }, + { + "name": "Wander through Neve Tzedek and Florentin", + "description": "Discover the trendy neighborhoods of Neve Tzedek and Florentin, known for their artistic vibe, boutique shops, and charming cafes. Explore the narrow streets, admire the street art, and soak up the bohemian atmosphere.", + "locationName": "Neve Tzedek and Florentin", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "tel-aviv", + "ref": "wander-through-neve-tzedek-and-florentin", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_wander-through-neve-tzedek-and-florentin.jpg" + }, + { + "name": "Experience the Tel Aviv Nightlife", + "description": "Tel Aviv comes alive at night with its vibrant bar and club scene. Dance the night away at one of the many beachfront clubs, enjoy live music at a local bar, or sip cocktails at a rooftop lounge with stunning city views.", + "locationName": "Rothschild Boulevard, Dizengoff Street", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "tel-aviv", + "ref": "experience-the-tel-aviv-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_experience-the-tel-aviv-nightlife.jpg" + }, + { + "name": "Indulge in Culinary Delights", + "description": "Tel Aviv offers a diverse culinary scene with options to satisfy every taste. From traditional Israeli cuisine to international flavors, explore the city's many restaurants, cafes, and food markets. Don't miss the opportunity to try hummus, falafel, and other local specialties.", + "locationName": "Carmel Market, Sarona Market", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tel-aviv", + "ref": "indulge-in-culinary-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_indulge-in-culinary-delights.jpg" + }, + { + "name": "Jaffa Old City Exploration", + "description": "Embark on a captivating journey through the ancient port city of Jaffa, a historical gem adjacent to Tel Aviv. Wander through its narrow cobblestone streets, discovering hidden alleyways, art galleries, and charming shops. Explore the vibrant Jaffa Flea Market, where you can find unique treasures and antiques. Visit the Ilana Goor Museum, housed in a restored 18th-century building, and admire its eclectic collection of art and artifacts. Immerse yourself in the rich history and cultural tapestry of this ancient city.", + "locationName": "Jaffa", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tel-aviv", + "ref": "jaffa-old-city-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_jaffa-old-city-exploration.jpg" + }, + { + "name": "Carmel Market Immersion", + "description": "Dive into the vibrant atmosphere of the Carmel Market, a bustling hub of sights, sounds, and aromas. Explore the labyrinthine stalls overflowing with fresh produce, spices, clothing, and souvenirs. Sample local delicacies like hummus, falafel, and freshly squeezed juices. Engage with friendly vendors and experience the authentic energy of Tel Aviv's daily life. The Carmel Market is a feast for the senses and a must-visit for any foodie or cultural enthusiast.", + "locationName": "Carmel Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "tel-aviv", + "ref": "carmel-market-immersion", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_carmel-market-immersion.jpg" + }, + { + "name": "Yarkon Park Cycling", + "description": "Escape the urban bustle and enjoy a leisurely bike ride through Yarkon Park, a sprawling green oasis in the heart of Tel Aviv. Rent a bike and explore the park's scenic paths, gardens, and lakes. Visit the Tropical Garden, the Rock Garden, and the Seven Mills. Enjoy a picnic lunch amidst nature or simply relax by the water. Yarkon Park offers a refreshing retreat and a chance to connect with nature.", + "locationName": "Yarkon Park", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "tel-aviv", + "ref": "yarkon-park-cycling", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_yarkon-park-cycling.jpg" + }, + { + "name": "Sarona Market Culinary Delights", + "description": "Indulge in a gastronomic adventure at Sarona Market, a trendy culinary destination featuring a diverse array of restaurants, cafes, and gourmet food stalls. Sample international cuisines, from Italian and Asian to Middle Eastern and Mediterranean. Explore artisanal cheese shops, bakeries, and spice shops. Enjoy a coffee or a glass of wine in a stylish setting. Sarona Market is a paradise for food lovers and offers a taste of Tel Aviv's cosmopolitan culinary scene.", + "locationName": "Sarona Market", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "tel-aviv", + "ref": "sarona-market-culinary-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_sarona-market-culinary-delights.jpg" + }, + { + "name": "Tel Aviv Museum of Art", + "description": "Immerse yourself in the world of art at the Tel Aviv Museum of Art, home to an extensive collection of Israeli and international works. Explore galleries showcasing modern and contemporary art, European masterpieces, and Israeli art from various periods. Admire works by renowned artists such as Van Gogh, Picasso, and Chagall. The museum also hosts temporary exhibitions and cultural events, providing a enriching experience for art enthusiasts.", + "locationName": "Tel Aviv Museum of Art", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "tel-aviv", + "ref": "tel-aviv-museum-of-art", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_tel-aviv-museum-of-art.jpg" + }, + { + "name": "Dive into History at the ANU Museum", + "description": "Embark on a captivating journey through time at the ANU - Museum of the Jewish People. Explore interactive exhibits that showcase the rich history, culture, and traditions of the Jewish people from around the world. Discover fascinating stories, artifacts, and multimedia displays that bring Jewish heritage to life.", + "locationName": "ANU - Museum of the Jewish People", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "tel-aviv", + "ref": "dive-into-history-at-the-anu-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_dive-into-history-at-the-anu-museum.jpg" + }, + { + "name": "Sunset Sail along the Mediterranean Coast", + "description": "Experience the magic of Tel Aviv from a different perspective with a breathtaking sunset sail along the Mediterranean coast. As the sun dips below the horizon, casting golden hues across the water, enjoy panoramic views of the city skyline and coastline while feeling the gentle sea breeze.", + "locationName": "Mediterranean Sea", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "tel-aviv", + "ref": "sunset-sail-along-the-mediterranean-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_sunset-sail-along-the-mediterranean-coast.jpg" + }, + { + "name": "Kayak Adventure on the Yarkon River", + "description": "Embark on an exciting kayaking adventure along the Yarkon River, a scenic waterway that winds its way through the heart of Tel Aviv. Paddle through tranquil waters, surrounded by lush greenery and urban landscapes, and discover hidden corners of the city from a unique perspective. ", + "locationName": "Yarkon River", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "tel-aviv", + "ref": "kayak-adventure-on-the-yarkon-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_kayak-adventure-on-the-yarkon-river.jpg" + }, + { + "name": "Shop at the Dizengoff Center", + "description": "Indulge in a shopping spree at the Dizengoff Center, a sprawling indoor mall featuring a wide array of shops, boutiques, and entertainment options. Explore the latest fashion trends, discover unique souvenirs, and enjoy a diverse selection of dining experiences.", + "locationName": "Dizengoff Center", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "tel-aviv", + "ref": "shop-at-the-dizengoff-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_shop-at-the-dizengoff-center.jpg" + }, + { + "name": "Escape to the Tranquility of Hayarkon Park", + "description": "Escape the bustling city life and find serenity in Hayarkon Park, a sprawling green oasis in the heart of Tel Aviv. Enjoy a leisurely stroll or bike ride along the park's scenic paths, have a picnic amidst the lush lawns, or rent a paddleboat and explore the serene lake.", + "locationName": "Hayarkon Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "tel-aviv", + "ref": "escape-to-the-tranquility-of-hayarkon-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_escape-to-the-tranquility-of-hayarkon-park.jpg" + }, + { + "name": "Explore the White City's Architectural Gems", + "description": "Embark on a captivating journey through Tel Aviv's UNESCO-listed White City, renowned for its exceptional collection of Bauhaus buildings. Join a guided walking tour or rent a bike to admire the unique architectural style, characterized by clean lines, white facades, and functional design. Discover iconic landmarks like the Bauhaus Center, Dizengoff Square, and Rothschild Boulevard, and immerse yourself in the city's rich architectural heritage.", + "locationName": "White City", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tel-aviv", + "ref": "explore-the-white-city-s-architectural-gems", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_explore-the-white-city-s-architectural-gems.jpg" + }, + { + "name": "Unwind in the Serene HaYarkon Park", + "description": "Escape the urban bustle and find tranquility in HaYarkon Park, a sprawling green oasis in the heart of Tel Aviv. Rent a paddleboat on the Yarkon River, enjoy a picnic on the grassy lawns, or explore the various gardens and walking paths. The park also offers sports facilities, a bird sanctuary, and an open-air concert venue, providing something for everyone.", + "locationName": "HaYarkon Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "tel-aviv", + "ref": "unwind-in-the-serene-hayarkon-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_unwind-in-the-serene-hayarkon-park.jpg" + }, + { + "name": "Discover History at the Palmach Museum", + "description": "Delve into Israel's fascinating history at the Palmach Museum, dedicated to the elite fighting force that played a crucial role in the country's War of Independence. Explore interactive exhibits, multimedia displays, and historical artifacts that tell the story of the Palmach's courage and sacrifice. Gain a deeper understanding of Israel's past and its fight for independence.", + "locationName": "Palmach Museum", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tel-aviv", + "ref": "discover-history-at-the-palmach-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_discover-history-at-the-palmach-museum.jpg" + }, + { + "name": "Experience the Levinsky Market Spice Trail", + "description": "Embark on a sensory adventure through the vibrant Levinsky Market, known for its colorful stalls brimming with spices, dried fruits, nuts, and other culinary delights. Join a guided food tour to learn about the history of the market, sample exotic flavors, and discover hidden culinary gems. Immerse yourself in the sights, smells, and tastes of this unique cultural experience.", + "locationName": "Levinsky Market", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "tel-aviv", + "ref": "experience-the-levinsky-market-spice-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_experience-the-levinsky-market-spice-trail.jpg" + }, + { + "name": "Catch a Performance at the Habima National Theatre", + "description": "Experience the magic of live theater at the Habima National Theatre, Israel's premier performing arts venue. Choose from a diverse repertoire of plays, musicals, and dance performances, showcasing local and international talent. Enjoy a captivating evening of entertainment in a beautiful and historic setting.", + "locationName": "Habima National Theatre", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "tel-aviv", + "ref": "catch-a-performance-at-the-habima-national-theatre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tel-aviv_catch-a-performance-at-the-habima-national-theatre.jpg" + }, + { + "name": "Explore the Historic City of Kazan", + "description": "Discover the rich history and cultural blend of Kazan, the capital of Tatarstan. Visit the Kazan Kremlin, a UNESCO World Heritage Site, and marvel at the Kul Sharif Mosque, a stunning example of Tatar architecture. Wander through the vibrant Bauman Street, known for its shops, restaurants, and street performers. Immerse yourself in the local culture by trying traditional Tatar dishes like echpochmak (triangular pastries filled with meat and potatoes) and chak-chak (a sweet honey pastry).", + "locationName": "Kazan", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "trans-siberian-railway", + "ref": "explore-the-historic-city-of-kazan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_explore-the-historic-city-of-kazan.jpg" + }, + { + "name": "Cruise on Lake Baikal", + "description": "Experience the beauty and serenity of Lake Baikal, the world's deepest and oldest freshwater lake. Take a boat cruise to explore the lake's crystal-clear waters, rugged coastlines, and diverse wildlife. Visit Olkhon Island, the largest island on Lake Baikal, known for its stunning natural landscapes and ancient shamanistic traditions. Hike to the top of Shaman Rock for panoramic views of the lake and surrounding mountains.", + "locationName": "Lake Baikal", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "cruise-on-lake-baikal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_cruise-on-lake-baikal.jpg" + }, + { + "name": "Visit the Winter Palace and Hermitage Museum in St. Petersburg", + "description": "Step into the opulent world of the Tsars at the Winter Palace, the former residence of Russian emperors. Explore the vast Hermitage Museum, one of the largest and most prestigious art museums in the world, housing an extensive collection of art and artifacts from around the globe. Admire masterpieces by renowned artists such as Leonardo da Vinci, Rembrandt, and Michelangelo. Immerse yourself in the grandeur and history of Russia's imperial past.", + "locationName": "St. Petersburg", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "trans-siberian-railway", + "ref": "visit-the-winter-palace-and-hermitage-museum-in-st-petersburg", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_visit-the-winter-palace-and-hermitage-museum-in-st-petersburg.jpg" + }, + { + "name": "Hike in the Altai Mountains", + "description": "Embark on a hiking adventure in the Altai Mountains, a remote and stunning mountain range in southern Siberia. Explore diverse landscapes, from alpine meadows and glaciers to dense forests and turquoise lakes. Challenge yourself with a multi-day trek or opt for a shorter hike to a scenic viewpoint. Immerse yourself in the tranquility of nature and experience the rugged beauty of the Altai region.", + "locationName": "Altai Mountains", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "hike-in-the-altai-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_hike-in-the-altai-mountains.jpg" + }, + { + "name": "Experience a Traditional Russian Banya", + "description": "Indulge in a relaxing and rejuvenating experience at a traditional Russian banya, a type of steam bath. Enjoy the heat and steam of the banya, followed by a refreshing plunge into cold water. Experience the therapeutic benefits of the banya, which is believed to improve circulation, boost the immune system, and promote relaxation.", + "locationName": "Various locations along the route", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "trans-siberian-railway", + "ref": "experience-a-traditional-russian-banya", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_experience-a-traditional-russian-banya.jpg" + }, + { + "name": "Explore Irkutsk, the 'Paris of Siberia'", + "description": "Discover the charming city of Irkutsk, known for its Siberian Baroque architecture, vibrant cultural scene, and proximity to Lake Baikal. Visit the 19th-century wooden houses, explore the Decembrist Museum, and wander through the bustling market.", + "locationName": "Irkutsk", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "trans-siberian-railway", + "ref": "explore-irkutsk-the-paris-of-siberia-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_explore-irkutsk-the-paris-of-siberia-.jpg" + }, + { + "name": "Go Dog Sledding in Siberia", + "description": "Experience the thrill of dog sledding through the snowy landscapes of Siberia. Learn about the traditional way of life for indigenous people and mush your own team of huskies across the frozen wilderness.", + "locationName": "Siberia", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "trans-siberian-railway", + "ref": "go-dog-sledding-in-siberia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_go-dog-sledding-in-siberia.jpg" + }, + { + "name": "Visit the Yekaterinburg Museum of Fine Arts", + "description": "Immerse yourself in Russian art at the Yekaterinburg Museum of Fine Arts. Admire a diverse collection of paintings, sculptures, and decorative arts, spanning from the 16th century to modern times.", + "locationName": "Yekaterinburg", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "trans-siberian-railway", + "ref": "visit-the-yekaterinburg-museum-of-fine-arts", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_visit-the-yekaterinburg-museum-of-fine-arts.jpg" + }, + { + "name": "Delve into the History of the Gulag at Perm-36", + "description": "Gain insight into the Soviet era with a visit to Perm-36, a former Gulag labor camp turned museum. Explore the preserved barracks, solitary confinement cells, and exhibits that document the harsh realities of the camp system.", + "locationName": "Perm-36", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "delve-into-the-history-of-the-gulag-at-perm-36", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_delve-into-the-history-of-the-gulag-at-perm-36.jpg" + }, + { + "name": "Take a Cooking Class and Learn to Make Pelmeni", + "description": "Discover the secrets of Russian cuisine with a hands-on cooking class. Learn how to make pelmeni, traditional Russian dumplings, and enjoy the fruits of your labor with a delicious meal.", + "locationName": "Various cities along the route", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "take-a-cooking-class-and-learn-to-make-pelmeni", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_take-a-cooking-class-and-learn-to-make-pelmeni.jpg" + }, + { + "name": "Visit the Golden Ring Cities", + "description": "Embark on a captivating detour from the main Trans-Siberian route to explore the Golden Ring, a collection of ancient towns northeast of Moscow. Immerse yourself in Russia's rich history and culture as you admire the iconic onion domes, kremlins, and monasteries in towns like Suzdal, Vladimir, and Sergiev Posad. Experience the charm of traditional Russian life, sample local crafts, and witness the architectural masterpieces that have stood for centuries.", + "locationName": "Golden Ring Cities (e.g., Suzdal, Vladimir, Sergiev Posad)", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "visit-the-golden-ring-cities", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_visit-the-golden-ring-cities.jpg" + }, + { + "name": "Explore the Ulan-Ude Buddhist Temples", + "description": "Step into a different world in Ulan-Ude, the capital of the Republic of Buryatia, where Tibetan Buddhism thrives. Visit the Ivolginsky Datsan, the largest Buddhist temple complex in Russia, and witness the intricate architecture, colorful prayer flags, and serene atmosphere. Engage with the monks, learn about Buddhist traditions, and experience a unique cultural fusion.", + "locationName": "Ulan-Ude", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "trans-siberian-railway", + "ref": "explore-the-ulan-ude-buddhist-temples", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_explore-the-ulan-ude-buddhist-temples.jpg" + }, + { + "name": "Go Ice Fishing on Lake Baikal", + "description": "For a true Siberian adventure, try ice fishing on the frozen surface of Lake Baikal, the world's deepest lake. Experience the thrill of drilling through the thick ice and casting your line into the crystal-clear waters. Learn about traditional ice fishing techniques from local guides and enjoy the tranquility of the winter landscape.", + "locationName": "Lake Baikal", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "go-ice-fishing-on-lake-baikal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_go-ice-fishing-on-lake-baikal.jpg" + }, + { + "name": "Visit the Novosibirsk Opera and Ballet Theatre", + "description": "Indulge in a night of culture and elegance at the Novosibirsk Opera and Ballet Theatre, one of the largest opera houses in the world. Witness world-class performances of classic operas, ballets, and contemporary productions in a stunning architectural setting. Dress up for the occasion and enjoy a taste of Russia's vibrant performing arts scene.", + "locationName": "Novosibirsk", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "trans-siberian-railway", + "ref": "visit-the-novosibirsk-opera-and-ballet-theatre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_visit-the-novosibirsk-opera-and-ballet-theatre.jpg" + }, + { + "name": "Take a Siberian Husky Sled Ride", + "description": "Experience the thrill of gliding through the snowy Siberian landscape on a sled pulled by a team of energetic Siberian Huskies. Enjoy the crisp winter air and the stunning scenery as you mush through forests and across frozen lakes. Learn about the history of dog sledding in Siberia and the special bond between mushers and their dogs.", + "locationName": "Various locations in Siberia", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "take-a-siberian-husky-sled-ride", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_take-a-siberian-husky-sled-ride.jpg" + }, + { + "name": "Visit the Kizhi Island Open-Air Museum", + "description": "Step back in time at the Kizhi Island Open-Air Museum, a UNESCO World Heritage Site showcasing traditional wooden architecture from the Karelia region. Marvel at the intricate craftsmanship of the Church of the Transfiguration with its 22 onion domes, explore historic windmills and peasant houses, and immerse yourself in the rich cultural heritage of the Russian north. This open-air museum offers a unique glimpse into rural life and architectural traditions.", + "locationName": "Kizhi Island, Lake Onega", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "visit-the-kizhi-island-open-air-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_visit-the-kizhi-island-open-air-museum.jpg" + }, + { + "name": "Explore the Charm of Yaroslavl", + "description": "Discover the historic city of Yaroslavl, one of the Golden Ring cities and a UNESCO World Heritage Site. Wander through the well-preserved historical center, admire the 16th-century Spassky Monastery and the Church of Elijah the Prophet, and soak in the vibrant atmosphere of the city's many squares and parks. Yaroslavl offers a delightful blend of history, culture, and architectural beauty.", + "locationName": "Yaroslavl", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "trans-siberian-railway", + "ref": "explore-the-charm-of-yaroslavl", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_explore-the-charm-of-yaroslavl.jpg" + }, + { + "name": "Go Hiking or Biking in the Ural Mountains", + "description": "Embark on an outdoor adventure in the Ural Mountains, the natural border between Europe and Asia. Hike through scenic trails, breathe in the fresh mountain air, and enjoy breathtaking views of the surrounding landscapes. For thrill-seekers, mountain biking offers an exhilarating way to experience the rugged beauty of the Urals. This activity is perfect for those seeking an active escape amidst stunning natural scenery.", + "locationName": "Ural Mountains", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "go-hiking-or-biking-in-the-ural-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_go-hiking-or-biking-in-the-ural-mountains.jpg" + }, + { + "name": "Visit the Taltsy Museum of Wooden Architecture", + "description": "Immerse yourself in Siberian history and culture at the Taltsy Museum, an open-air museum showcasing traditional wooden architecture from the 17th to 20th centuries. Explore relocated historical buildings, including peasant houses, a watchtower, and a watermill, and learn about the way of life of Siberian people in times gone by. The museum offers a fascinating journey through the region's past.", + "locationName": "Taltsy Museum, near Irkutsk", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "trans-siberian-railway", + "ref": "visit-the-taltsy-museum-of-wooden-architecture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_visit-the-taltsy-museum-of-wooden-architecture.jpg" + }, + { + "name": "Sample Local Cuisine and Vodka", + "description": "Embark on a culinary journey and savor the flavors of Russia. Indulge in traditional dishes like borscht, pelmeni, and beef stroganoff, and sample a variety of local vodkas. Whether you choose a cozy restaurant or a lively food market, this experience is a must for food enthusiasts looking to explore the rich culinary heritage of the region.", + "locationName": "Various locations along the Trans-Siberian Railway", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "trans-siberian-railway", + "ref": "sample-local-cuisine-and-vodka", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/trans-siberian-railway_sample-local-cuisine-and-vodka.jpg" + }, + { + "name": "Explore Bran Castle", + "description": "Step into the legendary Bran Castle, perched high on a rocky cliff. This iconic landmark, often associated with Dracula, offers a glimpse into medieval history with its Gothic architecture, secret passages, and intriguing exhibits. Explore the castle's chambers, learn about Vlad the Impaler's connection to the legend, and enjoy breathtaking views of the surrounding landscape.", + "locationName": "Bran Castle", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "transylvania", + "ref": "explore-bran-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_explore-bran-castle.jpg" + }, + { + "name": "Hike in the Carpathian Mountains", + "description": "Embark on a scenic hike through the stunning Carpathian Mountains. Choose from various trails, ranging from easy walks to challenging climbs, and discover picturesque landscapes, hidden waterfalls, and diverse flora and fauna. Keep an eye out for wildlife like brown bears, wolves, and lynx as you immerse yourself in the natural beauty of the region.", + "locationName": "Carpathian Mountains", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "hike-in-the-carpathian-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_hike-in-the-carpathian-mountains.jpg" + }, + { + "name": "Wander through Sibiu's Old Town", + "description": "Get lost in the charming streets of Sibiu's Old Town, a UNESCO World Heritage Site. Admire the colorful medieval houses, explore the historic squares, and visit impressive landmarks like the Brukenthal Palace and the Evangelical Cathedral. Enjoy the relaxed atmosphere, indulge in delicious Romanian cuisine at local restaurants, and discover unique souvenirs at craft shops.", + "locationName": "Sibiu", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "wander-through-sibiu-s-old-town", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_wander-through-sibiu-s-old-town.jpg" + }, + { + "name": "Discover the Fortified Churches of Transylvania", + "description": "Embark on a journey through history and visit the unique fortified churches of Transylvania. These UNESCO-listed Saxon villages boast impressive churches surrounded by fortified walls, offering a glimpse into the region's past. Explore villages like Biertan, Viscri, and Prejmer, admire the architectural marvels, and learn about the Saxon heritage of Transylvania.", + "locationName": "Various villages in Transylvania", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "transylvania", + "ref": "discover-the-fortified-churches-of-transylvania", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_discover-the-fortified-churches-of-transylvania.jpg" + }, + { + "name": "Indulge in a Traditional Romanian Feast", + "description": "Treat your taste buds to a delicious Romanian feast at a local restaurant or guesthouse. Savor traditional dishes like sarmale (stuffed cabbage rolls), mici (grilled minced meat rolls), and papanasi (fried doughnuts with sour cream and jam). Enjoy the warm hospitality and immerse yourself in the culinary culture of Transylvania.", + "locationName": "Various restaurants and guesthouses throughout Transylvania", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "indulge-in-a-traditional-romanian-feast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_indulge-in-a-traditional-romanian-feast.jpg" + }, + { + "name": "Bear Watching in the Carpathian Mountains", + "description": "Embark on a thrilling wildlife adventure in the Carpathian Mountains, home to one of Europe's largest brown bear populations. Join a guided tour and observe these majestic creatures in their natural habitat, learning about their behavior and conservation efforts. Witnessing these magnificent animals up close is an unforgettable experience.", + "locationName": "Carpathian Mountains", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "transylvania", + "ref": "bear-watching-in-the-carpathian-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_bear-watching-in-the-carpathian-mountains.jpg" + }, + { + "name": "Scenic Drive on the Transfagarasan Highway", + "description": "Experience one of the most breathtaking drives in Europe on the Transfagarasan Highway. This winding mountain road offers stunning vistas of the Carpathian Mountains, with dramatic landscapes, cascading waterfalls, and glacial lakes. Stop at Balea Lake for a cable car ride to the summit and enjoy panoramic views.", + "locationName": "Transfagarasan Highway", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "scenic-drive-on-the-transfagarasan-highway", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_scenic-drive-on-the-transfagarasan-highway.jpg" + }, + { + "name": "Relaxation and Rejuvenation at a Thermal Spa", + "description": "Indulge in a day of pampering and relaxation at one of Transylvania's renowned thermal spas. Experience the therapeutic benefits of mineral-rich waters, enjoy various spa treatments, and unwind in serene surroundings. Popular options include the Baile Felix and Sovata resorts, known for their healing properties and beautiful landscapes.", + "locationName": "Baile Felix or Sovata", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "transylvania", + "ref": "relaxation-and-rejuvenation-at-a-thermal-spa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_relaxation-and-rejuvenation-at-a-thermal-spa.jpg" + }, + { + "name": "Caving Adventure in the Apuseni Mountains", + "description": "Explore the hidden world beneath the surface with a caving adventure in the Apuseni Mountains. Discover stunning cave formations, underground rivers, and unique geological features. Join a guided tour to navigate the caves safely and learn about their fascinating history and ecosystem. This thrilling experience is perfect for adventurous travelers.", + "locationName": "Apuseni Mountains", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "transylvania", + "ref": "caving-adventure-in-the-apuseni-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_caving-adventure-in-the-apuseni-mountains.jpg" + }, + { + "name": "Wine Tasting in the Jidvei Wine Region", + "description": "Discover the rich flavors of Romanian wine with a visit to the Jidvei wine region. Explore the vineyards, learn about the winemaking process, and enjoy guided tastings of local varieties. Savor the unique character of Transylvanian wines, from crisp whites to full-bodied reds, and experience the region's viticultural heritage.", + "locationName": "Jidvei", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 2, + "destinationRef": "transylvania", + "ref": "wine-tasting-in-the-jidvei-wine-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_wine-tasting-in-the-jidvei-wine-region.jpg" + }, + { + "name": "Horseback Riding in the Transylvanian Countryside", + "description": "Embark on a horseback riding adventure through the picturesque Transylvanian countryside. Explore rolling hills, meadows, and forests, taking in the fresh air and stunning scenery. This activity offers a unique way to connect with nature and experience the traditional rural lifestyle of the region.", + "locationName": "Various locations throughout Transylvania", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "transylvania", + "ref": "horseback-riding-in-the-transylvanian-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_horseback-riding-in-the-transylvanian-countryside.jpg" + }, + { + "name": "Visit the UNESCO-listed Villages with Fortified Churches", + "description": "Explore the unique UNESCO-listed villages with fortified churches, such as Biertan, Viscri, and Saschiz. These villages offer a glimpse into the history and culture of the Transylvanian Saxons, with their impressive fortifications and well-preserved medieval architecture.", + "locationName": "Biertan, Viscri, Saschiz", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "visit-the-unesco-listed-villages-with-fortified-churches", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_visit-the-unesco-listed-villages-with-fortified-churches.jpg" + }, + { + "name": "Attend a Traditional Folk Festival", + "description": "Immerse yourself in the vibrant culture of Transylvania by attending a traditional folk festival. Experience lively music, colorful costumes, and energetic dances, and learn about the region's rich folklore and customs.", + "locationName": "Various locations throughout Transylvania", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "attend-a-traditional-folk-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_attend-a-traditional-folk-festival.jpg" + }, + { + "name": "Explore the Turda Salt Mine", + "description": "Descend into the depths of the Turda Salt Mine, a unique underground world with a fascinating history. Marvel at the impressive salt formations, take a boat ride on the subterranean lake, and enjoy the therapeutic benefits of the salty air.", + "locationName": "Turda", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "explore-the-turda-salt-mine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_explore-the-turda-salt-mine.jpg" + }, + { + "name": "Visit the Corvin Castle", + "description": "Explore the magnificent Corvin Castle, also known as Hunyadi Castle, a Gothic-Renaissance masterpiece with a rich history dating back to the 15th century. Discover its impressive architecture, legends of Vlad the Impaler, and captivating stories of medieval times.", + "locationName": "Hunedoara", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "visit-the-corvin-castle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_visit-the-corvin-castle.jpg" + }, + { + "name": "Cycle through the picturesque countryside", + "description": "Embark on a cycling adventure through the rolling hills and charming villages of Transylvania. Pedal past sunflower fields, vineyards, and ancient forests, taking in the fresh air and stunning scenery. Choose from various routes suitable for different fitness levels, and discover hidden gems along the way.", + "locationName": "Transylvanian Countryside", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "cycle-through-the-picturesque-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_cycle-through-the-picturesque-countryside.jpg" + }, + { + "name": "Kayaking on the Danube River", + "description": "Experience the tranquility of the Danube River on a kayaking excursion. Paddle along the scenic waterways, surrounded by lush landscapes and diverse wildlife. Enjoy the peaceful atmosphere and observe the natural beauty of the Danube Delta, a UNESCO World Heritage Site.", + "locationName": "Danube Delta", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "transylvania", + "ref": "kayaking-on-the-danube-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_kayaking-on-the-danube-river.jpg" + }, + { + "name": "Stargazing in the Carpathian Mountains", + "description": "Escape the city lights and immerse yourself in the breathtaking night sky of the Carpathian Mountains. Join a stargazing tour led by experienced astronomers, who will guide you through the constellations and share fascinating stories about the cosmos. Witness the Milky Way in all its glory and marvel at the vastness of the universe.", + "locationName": "Carpathian Mountains", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "transylvania", + "ref": "stargazing-in-the-carpathian-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_stargazing-in-the-carpathian-mountains.jpg" + }, + { + "name": "Photography tour of Saxon villages", + "description": "Capture the unique charm of Transylvanian Saxon villages on a photography tour. Explore the well-preserved architecture, colorful houses, and fortified churches, learning about the history and culture of these communities. Receive expert guidance from a professional photographer and create stunning images that will preserve your memories of this enchanting region.", + "locationName": "Saxon Villages", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "transylvania", + "ref": "photography-tour-of-saxon-villages", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_photography-tour-of-saxon-villages.jpg" + }, + { + "name": "Attend a traditional craft workshop", + "description": "Immerse yourself in the rich cultural heritage of Transylvania by participating in a traditional craft workshop. Learn the art of pottery, woodcarving, or weaving from skilled artisans and create your own unique souvenir. Gain insights into the region's artistic traditions and take home a piece of Transylvanian craftsmanship.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "transylvania", + "ref": "attend-a-traditional-craft-workshop", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/transylvania_attend-a-traditional-craft-workshop.jpg" + }, + { + "name": "Explore the Tulum Archaeological Site", + "description": "Step back in time and immerse yourself in Mayan history at the Tulum Archaeological Site. Wander through the ancient ruins, marvel at the iconic El Castillo temple perched on a cliff overlooking the turquoise Caribbean Sea, and imagine life in this once-thriving Mayan port city.", + "locationName": "Tulum Archaeological Site", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "tulum", + "ref": "explore-the-tulum-archaeological-site", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_explore-the-tulum-archaeological-site.jpg" + }, + { + "name": "Relax on Tulum's Pristine Beaches", + "description": "Unwind on the soft white sand beaches of Tulum, where the gentle waves of the Caribbean Sea meet the shore. Soak up the sun, swim in the crystal-clear waters, or simply relax under a palm tree with a refreshing drink. For a more secluded experience, head to Playa Paraíso or Playa Rucondido.", + "locationName": "Tulum Beaches", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "tulum", + "ref": "relax-on-tulum-s-pristine-beaches", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_relax-on-tulum-s-pristine-beaches.jpg" + }, + { + "name": "Dive into Cenotes", + "description": "Embark on a unique adventure and explore the mesmerizing cenotes, natural sinkholes filled with crystal-clear freshwater. Snorkel or scuba dive through these enchanting underwater worlds, discovering hidden caves, stalactites, and stalagmites. Gran Cenote and Cenote Dos Ojos are popular choices, offering unforgettable experiences for all levels.", + "locationName": "Gran Cenote or Cenote Dos Ojos", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "tulum", + "ref": "dive-into-cenotes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_dive-into-cenotes.jpg" + }, + { + "name": "Indulge in a Wellness Retreat", + "description": "Escape the stress of everyday life and rejuvenate your mind, body, and soul at one of Tulum's renowned wellness retreats. Practice yoga overlooking the ocean, indulge in spa treatments inspired by Mayan traditions, and connect with nature in a serene and tranquil environment.", + "locationName": "Various wellness centers and resorts", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "tulum", + "ref": "indulge-in-a-wellness-retreat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_indulge-in-a-wellness-retreat.jpg" + }, + { + "name": "Savor Culinary Delights", + "description": "Embark on a culinary journey and explore Tulum's diverse gastronomic scene. From beachfront restaurants serving fresh seafood to trendy cafes offering organic and sustainable cuisine, there's something to tantalize every palate. Don't miss the chance to try traditional Mayan dishes and experience the unique flavors of the region.", + "locationName": "Various restaurants and cafes in Tulum", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "tulum", + "ref": "savor-culinary-delights", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_savor-culinary-delights.jpg" + }, + { + "name": "Jungle Maya Native Park Adventure", + "description": "Embark on a thrilling journey into the heart of the Mayan jungle. Zip-line through the lush canopy, rappel into hidden cenotes for a refreshing swim, and participate in an authentic Mayan blessing ceremony. This action-packed experience offers a unique blend of adventure and cultural immersion.", + "locationName": "Jungle Maya Native Park", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "tulum", + "ref": "jungle-maya-native-park-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_jungle-maya-native-park-adventure.jpg" + }, + { + "name": "Sunset Horseback Riding on the Beach", + "description": "Experience the magic of Tulum's coastline on horseback as the sun dips below the horizon. Ride along the pristine beach, feeling the gentle sea breeze and witnessing the breathtaking colors of the sky. This romantic and unforgettable activity is perfect for couples or anyone seeking a serene connection with nature.", + "locationName": "Tulum Beach", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "tulum", + "ref": "sunset-horseback-riding-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_sunset-horseback-riding-on-the-beach.jpg" + }, + { + "name": "Sian Ka'an Biosphere Reserve Tour", + "description": "Discover the incredible biodiversity of the Sian Ka'an Biosphere Reserve, a UNESCO World Heritage Site. Explore its intricate network of lagoons, mangroves, and tropical forests by boat, encountering diverse wildlife such as dolphins, turtles, and exotic birds. This eco-conscious tour provides a glimpse into the region's rich natural heritage.", + "locationName": "Sian Ka'an Biosphere Reserve", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "tulum", + "ref": "sian-ka-an-biosphere-reserve-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_sian-ka-an-biosphere-reserve-tour.jpg" + }, + { + "name": "Tulum Art Walk and Shopping", + "description": "Immerse yourself in Tulum's vibrant art scene by strolling through its art galleries and boutiques. Discover unique handcrafted jewelry, textiles, and paintings by local artisans. This leisurely activity is perfect for finding special souvenirs and supporting the community's creative spirit.", + "locationName": "Tulum Town", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "tulum", + "ref": "tulum-art-walk-and-shopping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_tulum-art-walk-and-shopping.jpg" + }, + { + "name": "Live Music and Nightlife", + "description": "Experience Tulum's energetic nightlife scene, with beach clubs and bars offering live music, DJs, and dancing under the stars. Enjoy cocktails and soak up the vibrant atmosphere as you connect with fellow travelers and locals. This adults-only activity is perfect for those seeking a lively and social evening.", + "locationName": "Tulum Beach", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "tulum", + "ref": "live-music-and-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_live-music-and-nightlife.jpg" + }, + { + "name": "Muyil River Float and Mayan Community Visit", + "description": "Embark on a unique cultural experience, floating down the crystal-clear Muyil River in the Sian Ka'an Biosphere Reserve. Interact with the local Mayan community, learning about their traditions, way of life, and connection to the natural environment. This activity provides a deeper understanding of the region's rich heritage.", + "locationName": "Sian Ka'an Biosphere Reserve", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "tulum", + "ref": "muyil-river-float-and-mayan-community-visit", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_muyil-river-float-and-mayan-community-visit.jpg" + }, + { + "name": "Bike Tour through Tulum's Pueblo", + "description": "Explore the vibrant town of Tulum on two wheels, cycling through its charming streets and discovering hidden gems. Visit local markets, street art murals, and authentic eateries, experiencing the town's unique atmosphere and local culture at your own pace.", + "locationName": "Tulum Pueblo", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tulum", + "ref": "bike-tour-through-tulum-s-pueblo", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_bike-tour-through-tulum-s-pueblo.jpg" + }, + { + "name": "Cooking Class with a Local Chef", + "description": "Delve into the world of Mexican cuisine with a hands-on cooking class. Learn traditional recipes and techniques from a local chef, preparing authentic dishes using fresh, regional ingredients. This immersive experience allows you to savor the flavors of Mexico and recreate them at home.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "tulum", + "ref": "cooking-class-with-a-local-chef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_cooking-class-with-a-local-chef.jpg" + }, + { + "name": "Temazcal Ceremony", + "description": "Participate in a traditional Temazcal ceremony, a Mayan steam bath ritual used for purification and spiritual cleansing. Led by a shaman, this ancient practice combines heat, herbs, and chanting to promote relaxation, detoxification, and a connection to nature.", + "locationName": "Various locations", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "tulum", + "ref": "temazcal-ceremony", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_temazcal-ceremony.jpg" + }, + { + "name": "Stargazing on the Beach", + "description": "Escape the city lights and experience the magic of Tulum's night sky. Join a guided stargazing tour on the beach, learning about constellations, planets, and the wonders of the universe. The clear, dark skies provide an unforgettable opportunity to connect with nature and marvel at the cosmos.", + "locationName": "Tulum Beaches", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 2, + "destinationRef": "tulum", + "ref": "stargazing-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_stargazing-on-the-beach.jpg" + }, + { + "name": "Coba Ruins Day Trip", + "description": "Embark on a journey to the ancient Mayan city of Coba, nestled deep within the jungle. Climb the Nohoch Mul pyramid, the tallest in the Yucatan Peninsula, and enjoy breathtaking panoramic views. Explore the vast complex of temples, ball courts, and sacbeob (ancient Mayan roads) by bicycle or on foot, and immerse yourself in the rich history and culture of this remarkable site.", + "locationName": "Coba", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "tulum", + "ref": "coba-ruins-day-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_coba-ruins-day-trip.jpg" + }, + { + "name": "Scuba Diving in the Mesoamerican Reef", + "description": "Dive into the turquoise waters of the Mesoamerican Reef, the second-largest coral reef system in the world. Discover an underwater paradise teeming with vibrant marine life, including colorful fish, graceful sea turtles, and majestic manta rays. Explore underwater caves and caverns, or visit the unique Museo Subacuático de Arte (MUSA), an underwater museum featuring sculptures that promote coral life.", + "locationName": "Mesoamerican Reef", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "tulum", + "ref": "scuba-diving-in-the-mesoamerican-reef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_scuba-diving-in-the-mesoamerican-reef.jpg" + }, + { + "name": "Kaan Luum Lagoon", + "description": "Escape the crowds and unwind in the serene beauty of Kaan Luum Lagoon. This hidden gem boasts crystal-clear waters with varying shades of blue and green, surrounded by lush mangroves. Float in the shallows, take a dip in the cenote at the lagoon's center, or simply relax in a hammock and soak up the tranquil atmosphere.", + "locationName": "Kaan Luum Lagoon", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tulum", + "ref": "kaan-luum-lagoon", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_kaan-luum-lagoon.jpg" + }, + { + "name": "Tulum Tower and Cenote Encantado", + "description": "Climb the iconic Tulum Tower, a watchtower built by the Mayans for coastal surveillance, and enjoy stunning views of the Caribbean Sea and the Tulum Archaeological Site. Afterwards, take a refreshing swim in the nearby Cenote Encantado, a hidden oasis with crystal-clear waters and a rope swing for adventurous souls.", + "locationName": "Tulum Tower and Cenote Encantado", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "tulum", + "ref": "tulum-tower-and-cenote-encantado", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_tulum-tower-and-cenote-encantado.jpg" + }, + { + "name": "Local Market and Street Food Tour", + "description": "Immerse yourself in the vibrant local culture with a visit to a Tulum market. Explore the colorful stalls filled with fresh produce, handcrafted souvenirs, and traditional Mexican clothing. Indulge in a culinary adventure by sampling delicious street food, such as tacos, tamales, and marquesitas, while interacting with friendly vendors and experiencing the authentic flavors of Mexico.", + "locationName": "Tulum Markets", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "tulum", + "ref": "local-market-and-street-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tulum_local-market-and-street-food-tour.jpg" + }, + { + "name": "Explore the Ancient City of Ephesus", + "description": "Step back in time and wander through the remarkably preserved ruins of Ephesus, once a thriving Roman city. Marvel at the Library of Celsus, the Temple of Hadrian, and the Great Theatre, imagining life in ancient times. Guided tours offer fascinating insights into the history and culture of this UNESCO World Heritage Site.", + "locationName": "Ephesus", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "turkish-riviera", + "ref": "explore-the-ancient-city-of-ephesus", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_explore-the-ancient-city-of-ephesus.jpg" + }, + { + "name": "Relax on the Beaches of Antalya", + "description": "Soak up the sun on the golden sands of Antalya's stunning beaches. Whether you choose the popular Konyaalti Beach or the secluded Kaputas Beach, you'll be treated to crystal-clear turquoise waters and breathtaking coastal views. Enjoy swimming, sunbathing, or simply relaxing with a good book.", + "locationName": "Antalya", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "turkish-riviera", + "ref": "relax-on-the-beaches-of-antalya", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_relax-on-the-beaches-of-antalya.jpg" + }, + { + "name": "Embark on a Blue Cruise", + "description": "Set sail on a traditional Turkish gulet and experience the beauty of the Turkish Riviera from the water. Cruise along the coastline, stopping at secluded coves, hidden beaches, and charming coastal towns. Enjoy swimming, snorkeling, and sunbathing on deck, while indulging in delicious Turkish cuisine prepared by the onboard chef.", + "locationName": "Turkish Riviera", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "turkish-riviera", + "ref": "embark-on-a-blue-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_embark-on-a-blue-cruise.jpg" + }, + { + "name": "Discover the Underwater World", + "description": "Dive into the crystal-clear waters of the Mediterranean and explore the vibrant marine life. The Turkish Riviera offers numerous diving and snorkeling spots, where you can encounter colorful fish, ancient shipwrecks, and fascinating underwater landscapes. Whether you're a beginner or an experienced diver, there's an underwater adventure waiting for you.", + "locationName": "Kaş or Kalkan", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "turkish-riviera", + "ref": "discover-the-underwater-world", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_discover-the-underwater-world.jpg" + }, + { + "name": "Indulge in a Turkish Bath Experience", + "description": "Treat yourself to a traditional Turkish bath, also known as a hammam. Experience the ultimate relaxation as you enjoy a steam bath, followed by a body scrub and a soothing massage. This centuries-old tradition is a perfect way to unwind and rejuvenate your body and mind.", + "locationName": "Turkish Riviera", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 2, + "destinationRef": "turkish-riviera", + "ref": "indulge-in-a-turkish-bath-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_indulge-in-a-turkish-bath-experience.jpg" + }, + { + "name": "Hike the Lycian Way", + "description": "Embark on a breathtaking journey along the Lycian Way, a 540-kilometer trail that winds through ancient ruins, secluded coves, and dramatic coastal cliffs. Choose from various sections based on your fitness level, and experience the region's natural beauty and rich history.", + "locationName": "Lycian Way", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "turkish-riviera", + "ref": "hike-the-lycian-way", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_hike-the-lycian-way.jpg" + }, + { + "name": "Go White Water Rafting on the Köprülü River", + "description": "Experience an adrenaline-pumping adventure with white water rafting on the Köprülü River. Navigate through thrilling rapids surrounded by stunning canyon scenery. This activity is perfect for adventure seekers and nature enthusiasts.", + "locationName": "Köprülü River", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "turkish-riviera", + "ref": "go-white-water-rafting-on-the-k-pr-l-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_go-white-water-rafting-on-the-k-pr-l-river.jpg" + }, + { + "name": "Explore the Vibrant City of Antalya", + "description": "Wander through the charming streets of Antalya, a bustling city with a rich history and vibrant culture. Visit Kaleiçi, the historic old town, with its Ottoman-era houses and charming shops. Explore Hadrian's Gate, a triumphal arch dating back to the Roman era, and discover the Antalya Museum with its impressive collection of artifacts.", + "locationName": "Antalya", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "turkish-riviera", + "ref": "explore-the-vibrant-city-of-antalya", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_explore-the-vibrant-city-of-antalya.jpg" + }, + { + "name": "Take a Cooking Class and Learn to Make Traditional Turkish Dishes", + "description": "Delve into the culinary world of Turkey by taking a cooking class. Learn to prepare authentic dishes like meze platters, kebabs, and baklava, under the guidance of local chefs. This immersive experience offers a delicious way to connect with Turkish culture.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "turkish-riviera", + "ref": "take-a-cooking-class-and-learn-to-make-traditional-turkish-dishes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_take-a-cooking-class-and-learn-to-make-traditional-turkish-dishes.jpg" + }, + { + "name": "Visit the Picturesque Town of Kaş", + "description": "Escape to the charming town of Kaş, known for its bohemian atmosphere, stunning beaches, and ancient Lycian ruins. Explore the narrow streets lined with boutique shops and cafes, or relax on the pebble beaches and enjoy the crystal-clear waters. For a unique experience, take a boat trip to the nearby Greek island of Meis.", + "locationName": "Kaş", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "turkish-riviera", + "ref": "visit-the-picturesque-town-of-ka-", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_visit-the-picturesque-town-of-ka-.jpg" + }, + { + "name": "Paraglide Over Ölüdeniz", + "description": "Experience the breathtaking beauty of Ölüdeniz from a bird's-eye view as you soar through the sky on a tandem paragliding adventure. Take off from the Babadağ Mountain and glide over the turquoise waters, sandy beaches, and lush landscapes, creating an unforgettable memory.", + "locationName": "Ölüdeniz", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 4, + "destinationRef": "turkish-riviera", + "ref": "paraglide-over-l-deniz", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_paraglide-over-l-deniz.jpg" + }, + { + "name": "Visit the Saklikent Gorge", + "description": "Embark on a journey to Saklikent Gorge, one of the deepest canyons in the world. Hike through the cool, rushing waters, admire the towering cliffs, and discover hidden waterfalls. This natural wonder offers a refreshing escape from the summer heat.", + "locationName": "Saklikent National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "turkish-riviera", + "ref": "visit-the-saklikent-gorge", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_visit-the-saklikent-gorge.jpg" + }, + { + "name": "Explore the Ghost Village of Kayaköy", + "description": "Step back in time and explore the haunting ruins of Kayaköy, a once-thriving Greek village abandoned in the early 20th century. Wander through the deserted streets, houses, and churches, and learn about the village's history and the population exchange between Greece and Turkey.", + "locationName": "Kayaköy", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "turkish-riviera", + "ref": "explore-the-ghost-village-of-kayak-y", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_explore-the-ghost-village-of-kayak-y.jpg" + }, + { + "name": "Shop at the Fethiye Market", + "description": "Immerse yourself in the vibrant atmosphere of the Fethiye Market, where you can find an array of local goods, including fresh produce, spices, textiles, and souvenirs. Practice your bargaining skills and discover unique treasures to take home.", + "locationName": "Fethiye", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "turkish-riviera", + "ref": "shop-at-the-fethiye-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_shop-at-the-fethiye-market.jpg" + }, + { + "name": "Enjoy a Traditional Turkish Night", + "description": "Experience the vibrant culture of Turkey with a traditional Turkish night. Enjoy a delicious dinner with local cuisine, watch captivating belly dancing performances, and listen to live music, creating a memorable evening of entertainment.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "turkish-riviera", + "ref": "enjoy-a-traditional-turkish-night", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_enjoy-a-traditional-turkish-night.jpg" + }, + { + "name": "Hot Air Balloon Ride Over Cappadocia", + "description": "Experience the breathtaking landscapes of Cappadocia from a unique perspective with a hot air balloon ride at sunrise. Soar above the fairy chimneys, valleys, and rock formations, capturing unforgettable photos and creating lasting memories. This magical experience is perfect for couples, families, and anyone seeking a once-in-a-lifetime adventure.", + "locationName": "Cappadocia", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "turkish-riviera", + "ref": "hot-air-balloon-ride-over-cappadocia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_hot-air-balloon-ride-over-cappadocia.jpg" + }, + { + "name": "Jeep Safari Adventure in the Taurus Mountains", + "description": "Embark on an exhilarating jeep safari through the rugged terrain of the Taurus Mountains. Explore hidden waterfalls, traditional villages, and stunning viewpoints. This adventurous activity is ideal for thrill-seekers and nature enthusiasts who want to experience the wild side of the Turkish Riviera.", + "locationName": "Taurus Mountains", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "turkish-riviera", + "ref": "jeep-safari-adventure-in-the-taurus-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_jeep-safari-adventure-in-the-taurus-mountains.jpg" + }, + { + "name": "Visit the Pamukkale Thermal Pools", + "description": "Discover the natural wonder of Pamukkale, with its cascading white travertine terraces and thermal pools. Take a dip in the mineral-rich waters, known for their healing properties, and enjoy the unique and picturesque landscape. This relaxing experience is perfect for unwinding and rejuvenating.", + "locationName": "Pamukkale", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "turkish-riviera", + "ref": "visit-the-pamukkale-thermal-pools", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_visit-the-pamukkale-thermal-pools.jpg" + }, + { + "name": "Explore the Ancient City of Perge", + "description": "Step back in time and explore the ruins of Perge, an ancient Greek city that was once a major center of trade and culture. Admire the well-preserved Hellenistic and Roman architecture, including the impressive theater, stadium, and agora. This historical experience is perfect for history buffs and those interested in ancient civilizations.", + "locationName": "Perge", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "turkish-riviera", + "ref": "explore-the-ancient-city-of-perge", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_explore-the-ancient-city-of-perge.jpg" + }, + { + "name": "Wine Tasting Tour in the Vineyards of Bozcaada", + "description": "Embark on a delightful wine tasting tour in the vineyards of Bozcaada, a charming island known for its wine production. Sample local wines, learn about the winemaking process, and enjoy the scenic beauty of the vineyards. This experience is perfect for wine enthusiasts and those seeking a relaxing and flavorful getaway.", + "locationName": "Bozcaada", + "duration": 5, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "turkish-riviera", + "ref": "wine-tasting-tour-in-the-vineyards-of-bozcaada", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/turkish-riviera_wine-tasting-tour-in-the-vineyards-of-bozcaada.jpg" + }, + { + "name": "Wine Tasting Tour in Chianti", + "description": "Embark on a delightful journey through the rolling hills of Chianti, famed for its world-renowned wines. Visit charming vineyards, learn about the winemaking process from passionate producers, and savor the rich flavors of Chianti Classico and other regional varieties. Enjoy breathtaking views of the Tuscan countryside and indulge in local delicacies paired perfectly with your wine.", + "locationName": "Chianti Region", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "tuscany", + "ref": "wine-tasting-tour-in-chianti", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_wine-tasting-tour-in-chianti.jpg" + }, + { + "name": "Explore the Historic City of Florence", + "description": "Step back in time as you wander through the enchanting streets of Florence, the birthplace of the Renaissance. Marvel at architectural masterpieces like the Duomo and Ponte Vecchio, visit world-class museums housing works by Michelangelo and Leonardo da Vinci, and immerse yourself in the vibrant culture of this historic city.", + "locationName": "Florence", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tuscany", + "ref": "explore-the-historic-city-of-florence", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_explore-the-historic-city-of-florence.jpg" + }, + { + "name": "Cooking Class in a Tuscan Farmhouse", + "description": "Unleash your inner chef with a hands-on cooking class in a traditional Tuscan farmhouse. Learn the secrets of authentic Italian cuisine from local experts, using fresh, seasonal ingredients. Create delicious dishes like handmade pasta, savory sauces, and delectable desserts. Enjoy the fruits of your labor with a convivial meal accompanied by regional wines.", + "locationName": "Tuscan Countryside", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "tuscany", + "ref": "cooking-class-in-a-tuscan-farmhouse", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_cooking-class-in-a-tuscan-farmhouse.jpg" + }, + { + "name": "Hot Air Balloon Ride over the Val d'Orcia", + "description": "Soar above the picturesque landscapes of the Val d'Orcia in a hot air balloon, taking in breathtaking panoramic views of rolling hills, vineyards, and charming villages. Experience the serenity of floating through the air as you witness the beauty of the Tuscan countryside from a unique perspective.", + "locationName": "Val d'Orcia", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "tuscany", + "ref": "hot-air-balloon-ride-over-the-val-d-orcia", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_hot-air-balloon-ride-over-the-val-d-orcia.jpg" + }, + { + "name": "Relaxing Spa Day in a Thermal Bath", + "description": "Indulge in a rejuvenating spa day at one of Tuscany's renowned thermal baths. Immerse yourself in the therapeutic waters, rich in minerals and known for their healing properties. Enjoy a variety of spa treatments, such as massages, facials, and body wraps, and experience ultimate relaxation amidst the serene Tuscan surroundings.", + "locationName": "Various locations", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "tuscany", + "ref": "relaxing-spa-day-in-a-thermal-bath", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_relaxing-spa-day-in-a-thermal-bath.jpg" + }, + { + "name": "Truffle Hunting in the Tuscan Countryside", + "description": "Embark on a unique adventure, joining local truffle hunters and their trained dogs to search for the prized delicacy among the oak and chestnut trees. Learn about the art of truffle hunting, the different types of truffles, and their culinary significance in Tuscan cuisine. This immersive experience often concludes with a delicious truffle-infused meal, allowing you to savor the fruits of your labor.", + "locationName": "Various locations in the Tuscan countryside", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "tuscany", + "ref": "truffle-hunting-in-the-tuscan-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_truffle-hunting-in-the-tuscan-countryside.jpg" + }, + { + "name": "Hiking or Biking through the Picturesque Landscapes", + "description": "Explore the breathtaking landscapes of Tuscany at your own pace by hiking or biking through its rolling hills, vineyards, and olive groves. Numerous trails cater to various fitness levels, offering stunning vistas and opportunities to discover hidden gems like charming villages, ancient ruins, and local farms. Pack a picnic lunch and enjoy a peaceful break amidst the idyllic scenery.", + "locationName": "Various locations throughout Tuscany, including the Chianti region, Val d'Orcia, and the Tuscan Archipelago", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tuscany", + "ref": "hiking-or-biking-through-the-picturesque-landscapes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_hiking-or-biking-through-the-picturesque-landscapes.jpg" + }, + { + "name": "Visit the Leaning Tower of Pisa and Explore Pisa", + "description": "Discover the iconic Leaning Tower of Pisa and explore the historic city that surrounds it. Capture memorable photos with the leaning tower, climb its steps for panoramic views, and visit the nearby Cathedral and Baptistery. Wander through Pisa's charming squares and streets, enjoying the local atmosphere and indulging in delicious Italian cuisine.", + "locationName": "Pisa", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tuscany", + "ref": "visit-the-leaning-tower-of-pisa-and-explore-pisa", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_visit-the-leaning-tower-of-pisa-and-explore-pisa.jpg" + }, + { + "name": "Take a Boat Trip to Elba Island", + "description": "Escape to the largest island of the Tuscan Archipelago, Elba, and discover its stunning beaches, crystal-clear waters, and rich history. Take a ferry or boat trip to the island, explore its charming towns and villages, relax on its sandy shores, or embark on a hike or bike ride through its scenic landscapes. Elba also offers opportunities for snorkeling, diving, and exploring historical sites like Napoleon's former residence.", + "locationName": "Elba Island", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "tuscany", + "ref": "take-a-boat-trip-to-elba-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_take-a-boat-trip-to-elba-island.jpg" + }, + { + "name": "Enjoy a Traditional Tuscan Dinner with a Local Family", + "description": "Immerse yourself in the local culture by enjoying a traditional Tuscan dinner with a welcoming family in their home. Savor authentic home-cooked dishes, learn about local customs and traditions, and share stories and laughter with your hosts. This experience offers a unique opportunity to connect with the people of Tuscany and create lasting memories.", + "locationName": "Various locations in the Tuscan countryside", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "tuscany", + "ref": "enjoy-a-traditional-tuscan-dinner-with-a-local-family", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_enjoy-a-traditional-tuscan-dinner-with-a-local-family.jpg" + }, + { + "name": "Horseback Riding through the Tuscan Hills", + "description": "Embark on a horseback riding adventure through the picturesque Tuscan countryside. Explore rolling hills, vineyards, and olive groves while enjoying the fresh air and stunning views. This activity is suitable for all levels of experience, and local guides will ensure a safe and enjoyable ride.", + "locationName": "Tuscan Countryside", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "tuscany", + "ref": "horseback-riding-through-the-tuscan-hills", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_horseback-riding-through-the-tuscan-hills.jpg" + }, + { + "name": "Visit the Medieval Town of San Gimignano", + "description": "Step back in time with a visit to the charming medieval town of San Gimignano, known as the 'Town of Fine Towers'. Explore the narrow streets, admire the historic architecture, and climb the towers for panoramic views of the surrounding countryside. Don't forget to sample the local gelato, renowned for its delicious flavors.", + "locationName": "San Gimignano", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "tuscany", + "ref": "visit-the-medieval-town-of-san-gimignano", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_visit-the-medieval-town-of-san-gimignano.jpg" + }, + { + "name": "Kayaking or Canoeing on the Arno River", + "description": "Experience Tuscany from a different perspective with a kayaking or canoeing trip on the Arno River. Paddle through the heart of Florence, passing under historic bridges and admiring the city's iconic landmarks from the water. This activity is a great way to combine sightseeing with a bit of exercise and enjoy the outdoors.", + "locationName": "Arno River", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "tuscany", + "ref": "kayaking-or-canoeing-on-the-arno-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_kayaking-or-canoeing-on-the-arno-river.jpg" + }, + { + "name": "Attend a Palio Horse Race in Siena", + "description": "Immerse yourself in the excitement of the Palio, a historic horse race held twice a year in Siena's Piazza del Campo. Witness the pageantry, the fierce competition between the city's districts, and the passionate crowds. This is a truly unique cultural experience that will leave you with lasting memories.", + "locationName": "Siena", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "tuscany", + "ref": "attend-a-palio-horse-race-in-siena", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_attend-a-palio-horse-race-in-siena.jpg" + }, + { + "name": "Go Stargazing in the Tuscan Countryside", + "description": "Escape the city lights and experience the magic of the Tuscan night sky. Join a stargazing tour or simply find a quiet spot away from light pollution. With minimal light interference, you'll have the opportunity to see a breathtaking display of stars and constellations.", + "locationName": "Tuscan Countryside", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "tuscany", + "ref": "go-stargazing-in-the-tuscan-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_go-stargazing-in-the-tuscan-countryside.jpg" + }, + { + "name": "Take a Vespa Tour through the Tuscan Countryside", + "description": "Embark on a thrilling adventure as you zip through the picturesque Tuscan countryside on a Vespa scooter. Feel the wind in your hair and soak up the breathtaking scenery as you explore hidden gems, charming villages, and rolling vineyards. This unique and exhilarating experience allows you to discover the true essence of Tuscany at your own pace.", + "locationName": "Tuscan Countryside", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "tuscany", + "ref": "take-a-vespa-tour-through-the-tuscan-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_take-a-vespa-tour-through-the-tuscan-countryside.jpg" + }, + { + "name": "Visit the Uffizi Gallery and Admire Renaissance Masterpieces", + "description": "Immerse yourself in the world of Renaissance art at the renowned Uffizi Gallery in Florence. Marvel at iconic works by legendary artists such as Leonardo da Vinci, Michelangelo, and Botticelli. Explore the vast collection and witness the evolution of art throughout the ages. This cultural experience is a must for art enthusiasts and history buffs.", + "locationName": "Uffizi Gallery, Florence", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "tuscany", + "ref": "visit-the-uffizi-gallery-and-admire-renaissance-masterpieces", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_visit-the-uffizi-gallery-and-admire-renaissance-masterpieces.jpg" + }, + { + "name": "Go Wine Tasting in the Chianti Classico Region", + "description": "Indulge in the rich flavors of Tuscany's world-famous wines with a visit to the Chianti Classico region. Explore charming wineries, learn about the winemaking process, and savor the distinct taste of Chianti Classico. Enjoy breathtaking vineyard views and discover the perfect bottle to take home as a souvenir.", + "locationName": "Chianti Classico Region", + "duration": 5, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "tuscany", + "ref": "go-wine-tasting-in-the-chianti-classico-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_go-wine-tasting-in-the-chianti-classico-region.jpg" + }, + { + "name": "Explore the Boboli Gardens and Discover a Renaissance Oasis", + "description": "Escape the hustle and bustle of the city and wander through the enchanting Boboli Gardens in Florence. Discover hidden fountains, sculptures, and grottoes as you explore this Renaissance masterpiece. Enjoy a peaceful picnic amidst the lush greenery and admire the panoramic views of the city.", + "locationName": "Boboli Gardens, Florence", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "tuscany", + "ref": "explore-the-boboli-gardens-and-discover-a-renaissance-oasis", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_explore-the-boboli-gardens-and-discover-a-renaissance-oasis.jpg" + }, + { + "name": "Attend a Traditional Opera Performance in a Historic Theater", + "description": "Experience the magic of Italian opera with a captivating performance in a historic theater. Immerse yourself in the drama, music, and costumes as you witness a timeless masterpiece. This cultural evening is a perfect way to appreciate the artistic heritage of Tuscany.", + "locationName": "Various theaters in Tuscany", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "tuscany", + "ref": "attend-a-traditional-opera-performance-in-a-historic-theater", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/tuscany_attend-a-traditional-opera-performance-in-a-historic-theater.jpg" + }, + { + "name": "Snorkeling at Trunk Bay", + "description": "Embark on an underwater adventure at Trunk Bay, renowned for its crystal-clear waters and vibrant coral reefs. Swim among colorful fish, graceful sea turtles, and other fascinating marine life. The underwater snorkeling trail makes it easy to explore the reef's wonders, even for beginners. **Tags: Beach, Snorkeling, Family-friendly**", + "locationName": "Trunk Bay, St. John", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "us-virgin-islands", + "ref": "snorkeling-at-trunk-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_snorkeling-at-trunk-bay.jpg" + }, + { + "name": "Exploring Historic Charlotte Amalie", + "description": "Step back in time with a stroll through the charming streets of Charlotte Amalie, the capital of St. Thomas. Discover colonial architecture, historic forts, and duty-free shopping. Visit Blackbeard's Castle for panoramic views and pirate lore, or explore the 99 Steps, a historic stairway leading to breathtaking vistas. **Tags: City, Historic, Sightseeing, Shopping**", + "locationName": "Charlotte Amalie, St. Thomas", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "us-virgin-islands", + "ref": "exploring-historic-charlotte-amalie", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_exploring-historic-charlotte-amalie.jpg" + }, + { + "name": "Sunset Sail with Cocktails", + "description": "Indulge in a romantic and unforgettable experience with a sunset sail along the coastline. Sip on tropical cocktails as you admire the vibrant hues of the setting sun painting the sky and reflecting on the turquoise waters. Capture stunning photos and create lasting memories. **Tags: Romantic, Relaxing, Nightlife, Luxury**", + "locationName": "Various Locations", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "us-virgin-islands", + "ref": "sunset-sail-with-cocktails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_sunset-sail-with-cocktails.jpg" + }, + { + "name": "Hiking the Reef Bay Trail", + "description": "Embark on a scenic hike through the lush rainforest of Virgin Islands National Park. The Reef Bay Trail leads you past ancient petroglyphs, sugar mill ruins, and breathtaking views of the Caribbean Sea. Descend to the Reef Bay Sugar Mill and explore the historic site. **Tags: Hiking, Historic, Off-the-beaten-path**", + "locationName": "Virgin Islands National Park, St. John", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "us-virgin-islands", + "ref": "hiking-the-reef-bay-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_hiking-the-reef-bay-trail.jpg" + }, + { + "name": "Kayaking in a Bioluminescent Bay", + "description": "Experience the magic of a bioluminescent bay, where the water glows with an ethereal blue light at night. Paddle through the mangrove forests and witness the mesmerizing spectacle of tiny organisms illuminating the water with each stroke of your kayak. **Tags: Adventure, Nightlife, Eco-conscious**", + "locationName": "Various Locations", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 3, + "destinationRef": "us-virgin-islands", + "ref": "kayaking-in-a-bioluminescent-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_kayaking-in-a-bioluminescent-bay.jpg" + }, + { + "name": "Explore the Virgin Islands National Park", + "description": "Immerse yourself in the natural beauty of St. John by exploring the Virgin Islands National Park. Hike through lush forests, discover hidden beaches, and encounter diverse wildlife. Visit the Annaberg Sugar Plantation ruins for a glimpse into the island's history.", + "locationName": "St. John", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "us-virgin-islands", + "ref": "explore-the-virgin-islands-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_explore-the-virgin-islands-national-park.jpg" + }, + { + "name": "Discover the Coral World Ocean Park", + "description": "Embark on an underwater adventure at Coral World Ocean Park on St. Thomas. Get up close to marine life in the Undersea Observatory, swim with dolphins, hand-feed stingrays, and explore the vibrant coral reefs through snorkeling or diving.", + "locationName": "St. Thomas", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "us-virgin-islands", + "ref": "discover-the-coral-world-ocean-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_discover-the-coral-world-ocean-park.jpg" + }, + { + "name": "Indulge in Duty-Free Shopping", + "description": "Take advantage of the duty-free shopping opportunities in Charlotte Amalie, St. Thomas. Browse through a wide selection of jewelry, perfumes, electronics, and local crafts. Find unique souvenirs and gifts to commemorate your trip.", + "locationName": "Charlotte Amalie", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "us-virgin-islands", + "ref": "indulge-in-duty-free-shopping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_indulge-in-duty-free-shopping.jpg" + }, + { + "name": "Sample Local Cuisine", + "description": "Embark on a culinary journey and savor the flavors of the Virgin Islands. Try local specialties like conch fritters, johnnycakes, and callaloo. Visit beachside shacks for fresh seafood or indulge in fine dining experiences with Caribbean-inspired cuisine.", + "locationName": "Various Locations", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "us-virgin-islands", + "ref": "sample-local-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_sample-local-cuisine.jpg" + }, + { + "name": "Go Island Hopping", + "description": "Embark on an island-hopping adventure and discover the unique charm of each island. Take a ferry or boat tour to explore St. John's pristine beaches, St. Croix's historic towns, or Water Island's secluded coves. Experience the diverse landscapes and cultural offerings of the Virgin Islands.", + "locationName": "Inter-Island", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "us-virgin-islands", + "ref": "go-island-hopping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_go-island-hopping.jpg" + }, + { + "name": "Windsurfing at Magens Bay", + "description": "Experience the thrill of gliding across the turquoise waters of Magens Bay, renowned as one of the world's most beautiful beaches. Rent windsurfing equipment and catch the trade winds for an exhilarating adventure. Lessons are available for beginners, while experienced windsurfers can enjoy the freedom of exploring the bay at their own pace.", + "locationName": "Magens Bay, St. Thomas", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "us-virgin-islands", + "ref": "windsurfing-at-magens-bay", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_windsurfing-at-magens-bay.jpg" + }, + { + "name": "Horseback Riding on the Beach", + "description": "Embark on a unique and unforgettable adventure with a horseback riding tour along the sandy shores. Several outfitters offer guided tours that cater to all skill levels, allowing you to connect with nature and experience the island's beauty from a different perspective. Enjoy the gentle rhythm of the horse's gait as you take in breathtaking coastal views.", + "locationName": "Various beaches on St. Thomas and St. Croix", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "us-virgin-islands", + "ref": "horseback-riding-on-the-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_horseback-riding-on-the-beach.jpg" + }, + { + "name": "Stargazing on a Secluded Beach", + "description": "Escape the city lights and immerse yourself in the magic of the Caribbean night sky. Find a secluded beach away from light pollution and marvel at the constellations above. The Virgin Islands offer exceptional stargazing opportunities due to their remote location and minimal light interference. Bring a blanket, relax on the sand, and let the celestial wonders captivate you.", + "locationName": "Secluded beaches on St. John or Water Island", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "us-virgin-islands", + "ref": "stargazing-on-a-secluded-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_stargazing-on-a-secluded-beach.jpg" + }, + { + "name": "Explore the Annaberg Sugar Plantation Ruins", + "description": "Step back in time and delve into the Virgin Islands' colonial past at the Annaberg Sugar Plantation Ruins on St. John. Explore the remnants of this historic site, including the factory, windmill, and slave quarters, and learn about the island's sugar production history and the lives of those who worked on the plantation.", + "locationName": "Annaberg Sugar Plantation, St. John", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "us-virgin-islands", + "ref": "explore-the-annaberg-sugar-plantation-ruins", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_explore-the-annaberg-sugar-plantation-ruins.jpg" + }, + { + "name": "Take a Cooking Class and Learn to Make Local Dishes", + "description": "Immerse yourself in the local culture by participating in a cooking class and learning to prepare traditional Virgin Islands dishes. Discover the secrets of Caribbean cuisine, from fresh seafood specialties to flavorful stews and desserts. Many local chefs offer hands-on classes where you can learn about the ingredients, techniques, and cultural significance of the dishes.", + "locationName": "Various locations on St. Thomas and St. Croix", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "us-virgin-islands", + "ref": "take-a-cooking-class-and-learn-to-make-local-dishes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_take-a-cooking-class-and-learn-to-make-local-dishes.jpg" + }, + { + "name": "Dive into History at Fort Christiansvaern", + "description": "Step back in time at Fort Christiansvaern, a meticulously preserved 18th-century Danish fort on the island of St. Croix. Explore the ramparts, barracks, and dungeons, and learn about the island's colonial past and its role in the transatlantic slave trade. Immerse yourself in the fascinating history and stunning architecture of this historic landmark.", + "locationName": "Christiansted, St. Croix", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "us-virgin-islands", + "ref": "dive-into-history-at-fort-christiansvaern", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_dive-into-history-at-fort-christiansvaern.jpg" + }, + { + "name": "Catch a Thrill with Watersports Galore", + "description": "Get your adrenaline pumping with an array of exciting watersports! Rent jet skis and zoom across the turquoise waters, try your hand at parasailing for breathtaking aerial views, or challenge yourself with windsurfing or kitesurfing. The options are endless for an action-packed day on the water.", + "locationName": "Various locations throughout the islands", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "us-virgin-islands", + "ref": "catch-a-thrill-with-watersports-galore", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_catch-a-thrill-with-watersports-galore.jpg" + }, + { + "name": "Indulge in a Culinary Journey", + "description": "Embark on a delectable culinary journey through the Virgin Islands. Join a food tour to discover hidden gems and local favorites, from savory seafood dishes to sweet tropical treats. Explore the vibrant markets, savor fresh island produce, and experience the unique flavors of Caribbean cuisine.", + "locationName": "Various locations throughout the islands", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "us-virgin-islands", + "ref": "indulge-in-a-culinary-journey", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_indulge-in-a-culinary-journey.jpg" + }, + { + "name": "Unwind with a Spa Retreat", + "description": "Escape the hustle and bustle with a rejuvenating spa experience. Treat yourself to a massage, facial, or body wrap using local ingredients like coconut oil and sea salt. Immerse yourself in tranquility and emerge feeling refreshed and revitalized.", + "locationName": "Various resorts and spas", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "us-virgin-islands", + "ref": "unwind-with-a-spa-retreat", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_unwind-with-a-spa-retreat.jpg" + }, + { + "name": "Experience the Nightlife", + "description": "As the sun sets, the islands come alive with vibrant nightlife. Dance the night away at beach bars and clubs, enjoy live music performances, or sip cocktails under the stars. From laid-back beach bars to lively nightclubs, there's something for everyone to enjoy after dark.", + "locationName": "Various locations throughout the islands", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "us-virgin-islands", + "ref": "experience-the-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/us-virgin-islands_experience-the-nightlife.jpg" + }, + { + "name": "Whale Watching Adventure", + "description": "Embark on an unforgettable whale watching tour from Victoria or Tofino. Witness majestic orcas, humpback whales, and other marine life in their natural habitat. Learn about their behavior and conservation efforts from experienced guides. This awe-inspiring experience is perfect for nature enthusiasts of all ages.", + "locationName": "Victoria or Tofino", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "vancouver-island", + "ref": "whale-watching-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_whale-watching-adventure.jpg" + }, + { + "name": "Pacific Rim National Park Reserve Exploration", + "description": "Immerse yourself in the wild beauty of Pacific Rim National Park Reserve. Hike through ancient rainforests, explore rugged coastlines, and relax on pristine beaches. Discover diverse ecosystems and encounter unique wildlife. The park offers trails for all levels, making it ideal for both casual walkers and experienced hikers.", + "locationName": "Pacific Rim National Park Reserve", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vancouver-island", + "ref": "pacific-rim-national-park-reserve-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_pacific-rim-national-park-reserve-exploration.jpg" + }, + { + "name": "Kayaking in Clayoquot Sound", + "description": "Paddle through the tranquil waters of Clayoquot Sound, a UNESCO Biosphere Reserve. Explore hidden coves, encounter marine life, and admire the stunning scenery. Kayak tours are available for various skill levels, providing a unique perspective of Vancouver Island's coastal beauty.", + "locationName": "Clayoquot Sound", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "vancouver-island", + "ref": "kayaking-in-clayoquot-sound", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_kayaking-in-clayoquot-sound.jpg" + }, + { + "name": "Victoria City Tour and Afternoon Tea", + "description": "Explore the charming city of Victoria, known for its British colonial architecture and vibrant culture. Visit iconic landmarks like the Fairmont Empress Hotel and the Royal BC Museum. Indulge in a traditional afternoon tea experience, complete with scones, pastries, and exquisite tea blends.", + "locationName": "Victoria", + "duration": 5, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "vancouver-island", + "ref": "victoria-city-tour-and-afternoon-tea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_victoria-city-tour-and-afternoon-tea.jpg" + }, + { + "name": "Surfing in Tofino", + "description": "Catch some waves in Tofino, a renowned surfing destination. With its consistent swells and stunning beaches, Tofino offers an unforgettable experience for surfers of all levels. Surfing lessons and rentals are readily available, making it easy to enjoy this exhilarating water sport.", + "locationName": "Tofino", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "vancouver-island", + "ref": "surfing-in-tofino", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_surfing-in-tofino.jpg" + }, + { + "name": "Hiking the West Coast Trail", + "description": "Embark on a multi-day backpacking adventure along the legendary West Coast Trail. Traverse through old-growth forests, across rugged beaches, and over suspension bridges, experiencing the raw beauty of Vancouver Island's wild Pacific coast. This challenging hike offers stunning ocean views, encounters with wildlife, and a sense of accomplishment for those who complete it.", + "locationName": "Pacific Rim National Park Reserve", + "duration": 60, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "vancouver-island", + "ref": "hiking-the-west-coast-trail", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_hiking-the-west-coast-trail.jpg" + }, + { + "name": "Exploring Butchart Gardens", + "description": "Immerse yourself in the vibrant colors and fragrant scents of Butchart Gardens, a world-renowned horticultural masterpiece. Stroll through themed gardens, marvel at the intricate floral displays, and enjoy afternoon tea in a charming setting. This family-friendly attraction offers a relaxing escape into a world of natural beauty.", + "locationName": "Victoria", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "vancouver-island", + "ref": "exploring-butchart-gardens", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_exploring-butchart-gardens.jpg" + }, + { + "name": "Ziplining Through the Forest Canopy", + "description": "Soar through the air on a thrilling zipline adventure, experiencing the rainforest from a unique perspective. Choose from various zipline courses, each offering breathtaking views and an adrenaline rush. This activity is perfect for adventure seekers looking for an exhilarating experience in nature.", + "locationName": "Various locations on Vancouver Island", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "vancouver-island", + "ref": "ziplining-through-the-forest-canopy", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_ziplining-through-the-forest-canopy.jpg" + }, + { + "name": "Storm Watching on the West Coast", + "description": "Witness the raw power of the Pacific Ocean during storm season. Find a cozy cabin or beachfront lodge and watch as massive waves crash against the shore, creating a dramatic spectacle. This unique experience is perfect for those seeking a connection with nature's untamed forces.", + "locationName": "Tofino, Ucluelet", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vancouver-island", + "ref": "storm-watching-on-the-west-coast", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_storm-watching-on-the-west-coast.jpg" + }, + { + "name": "Caving in Horne Lake Caves Provincial Park", + "description": "Embark on a subterranean adventure exploring the Horne Lake Caves. Join a guided tour to discover stunning cave formations, underground rivers, and unique geological wonders. This activity is perfect for those seeking a unique and educational experience.", + "locationName": "Horne Lake Caves Provincial Park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vancouver-island", + "ref": "caving-in-horne-lake-caves-provincial-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_caving-in-horne-lake-caves-provincial-park.jpg" + }, + { + "name": "Bear Watching Tour in the Great Bear Rainforest", + "description": "Embark on a thrilling expedition into the heart of the Great Bear Rainforest, home to the majestic grizzly bears. Join a guided tour from Campbell River or Telegraph Cove and witness these magnificent creatures in their natural habitat as they fish for salmon and roam the pristine wilderness. This unforgettable experience offers a unique glimpse into the lives of these iconic animals.", + "locationName": "Great Bear Rainforest", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "vancouver-island", + "ref": "bear-watching-tour-in-the-great-bear-rainforest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_bear-watching-tour-in-the-great-bear-rainforest.jpg" + }, + { + "name": "Wine Tasting Tour in the Cowichan Valley", + "description": "Discover the burgeoning wine scene of the Cowichan Valley, known for its cool-climate wines and picturesque vineyards. Embark on a delightful wine tasting tour, visiting charming wineries and indulging in award-winning vintages. Learn about the unique terroir and winemaking process while savoring the flavors of Pinot Noir, Chardonnay, and other local specialties. This experience is perfect for wine enthusiasts and those seeking a relaxing escape.", + "locationName": "Cowichan Valley", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "vancouver-island", + "ref": "wine-tasting-tour-in-the-cowichan-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_wine-tasting-tour-in-the-cowichan-valley.jpg" + }, + { + "name": "Scenic Drive Along the Pacific Marine Circle Route", + "description": "Embark on a breathtaking road trip along the Pacific Marine Circle Route, a scenic loop that showcases the diverse beauty of Vancouver Island. Drive through charming coastal towns, witness dramatic ocean vistas, and explore hidden coves and beaches. Stop at Sooke Potholes Provincial Park for a refreshing swim in natural rock pools, or visit the quaint village of Cowichan Bay for local arts and crafts. This self-guided adventure offers flexibility and stunning views at every turn.", + "locationName": "Pacific Marine Circle Route", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vancouver-island", + "ref": "scenic-drive-along-the-pacific-marine-circle-route", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_scenic-drive-along-the-pacific-marine-circle-route.jpg" + }, + { + "name": "Mountain Biking in Cumberland", + "description": "Experience the thrill of mountain biking in Cumberland, a renowned destination for off-road enthusiasts. Explore the extensive network of trails that wind through lush forests and offer breathtaking views. Whether you're a beginner or an experienced rider, Cumberland has trails for all skill levels. Rent a bike and gear from local shops and immerse yourself in the vibrant mountain biking culture.", + "locationName": "Cumberland", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "vancouver-island", + "ref": "mountain-biking-in-cumberland", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_mountain-biking-in-cumberland.jpg" + }, + { + "name": "Relaxing Spa Day at a Resort", + "description": "Indulge in a rejuvenating spa day at one of Vancouver Island's luxurious resorts. Pamper yourself with a variety of treatments, from massages and facials to body wraps and hydrotherapy. Enjoy the serene atmosphere and stunning natural surroundings as you unwind and recharge. Several resorts offer spa packages, perfect for a romantic getaway or a solo escape.", + "locationName": "Various Resorts", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "vancouver-island", + "ref": "relaxing-spa-day-at-a-resort", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_relaxing-spa-day-at-a-resort.jpg" + }, + { + "name": "Go Salmon Fishing on the Campbell River", + "description": "Experience the thrill of reeling in a mighty salmon on the legendary Campbell River, renowned as the 'Salmon Capital of the World.' Charter a boat with a seasoned guide who'll lead you to the best spots and share their expertise. Whether you're a seasoned angler or a novice, this adventure promises excitement and a chance to connect with Vancouver Island's rich fishing heritage.", + "locationName": "Campbell River", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "vancouver-island", + "ref": "go-salmon-fishing-on-the-campbell-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_go-salmon-fishing-on-the-campbell-river.jpg" + }, + { + "name": "Explore the Culinary Scene in Victoria", + "description": "Embark on a delectable journey through Victoria's vibrant culinary scene. Discover farm-to-table restaurants showcasing the island's fresh produce, indulge in artisan chocolates and craft breweries, and savor the multicultural flavors that make Victoria a foodie paradise. Don't miss the chance to visit the iconic Fairmont Empress for a classic afternoon tea experience.", + "locationName": "Victoria", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "vancouver-island", + "ref": "explore-the-culinary-scene-in-victoria", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_explore-the-culinary-scene-in-victoria.jpg" + }, + { + "name": "Discover Indigenous Culture at the Royal BC Museum", + "description": "Immerse yourself in the rich history and culture of the First Nations people at the Royal BC Museum. Explore captivating exhibits that showcase traditional art, artifacts, and storytelling, and gain a deeper understanding of their deep connection to the land and sea. Participate in workshops or cultural events for a truly immersive experience.", + "locationName": "Victoria", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vancouver-island", + "ref": "discover-indigenous-culture-at-the-royal-bc-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_discover-indigenous-culture-at-the-royal-bc-museum.jpg" + }, + { + "name": "Take a Scenic Seaplane Flight", + "description": "Soar above Vancouver Island's breathtaking landscapes on a scenic seaplane flight. Witness the majesty of snow-capped mountains, turquoise waters, and lush forests from a unique perspective. Choose from various routes, including flights over the Gulf Islands, Tofino's coastline, or the remote Clayoquot Sound, for an unforgettable aerial adventure.", + "locationName": "Multiple locations", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "vancouver-island", + "ref": "take-a-scenic-seaplane-flight", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_take-a-scenic-seaplane-flight.jpg" + }, + { + "name": "Go Birdwatching at the Reifel Migratory Bird Sanctuary", + "description": "Escape to the tranquil oasis of the Reifel Migratory Bird Sanctuary, a haven for over 250 bird species. Stroll along the boardwalks and observe a diverse array of waterfowl, shorebirds, and songbirds in their natural habitat. Capture stunning photos, learn about bird conservation efforts, and enjoy the serene beauty of this wildlife sanctuary.", + "locationName": "Reifel Migratory Bird Sanctuary", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "vancouver-island", + "ref": "go-birdwatching-at-the-reifel-migratory-bird-sanctuary", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vancouver-island_go-birdwatching-at-the-reifel-migratory-bird-sanctuary.jpg" + }, + { + "name": "Schönbrunn Palace Tour", + "description": "Step into the opulent world of Habsburg royalty with a tour of the magnificent Schönbrunn Palace. Explore the lavish staterooms, stroll through the sprawling gardens, and immerse yourself in the history of the Austrian Empire.", + "locationName": "Schönbrunn Palace", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "vienna", + "ref": "sch-nbrunn-palace-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_sch-nbrunn-palace-tour.jpg" + }, + { + "name": "Vienna State Opera Performance", + "description": "Experience the magic of a world-renowned opera or ballet performance at the Vienna State Opera. Witness exceptional artistry and acoustics in a breathtaking setting, a truly unforgettable evening.", + "locationName": "Vienna State Opera", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "vienna", + "ref": "vienna-state-opera-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_vienna-state-opera-performance.jpg" + }, + { + "name": "Naschmarkt Culinary Adventure", + "description": "Embark on a sensory journey through the vibrant Naschmarkt, Vienna's largest market. Sample diverse flavors from around the world, from local Austrian specialties to exotic spices and fresh produce.", + "locationName": "Naschmarkt", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vienna", + "ref": "naschmarkt-culinary-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_naschmarkt-culinary-adventure.jpg" + }, + { + "name": "Danube River Cruise", + "description": "Enjoy a scenic cruise along the Danube River, admiring the city's iconic landmarks from a unique perspective. Relax and soak in the picturesque views while learning about Vienna's history and culture.", + "locationName": "Danube River", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vienna", + "ref": "danube-river-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_danube-river-cruise.jpg" + }, + { + "name": "MuseumsQuartier Exploration", + "description": "Discover a vibrant hub of art and culture at the MuseumsQuartier. Explore renowned museums like the Leopold Museum and MUMOK, showcasing modern and contemporary art, or simply relax in the trendy courtyards and cafes.", + "locationName": "MuseumsQuartier", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vienna", + "ref": "museumsquartier-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_museumsquartier-exploration.jpg" + }, + { + "name": "Prater Amusement Park", + "description": "Experience thrills and laughter at Prater, Vienna's iconic amusement park! Ride the Wiener Riesenrad Ferris wheel for panoramic city views, test your courage on roller coasters, and enjoy classic carnival games and treats. This historic park offers entertainment for all ages, making it a perfect family outing.", + "locationName": "Prater", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "vienna", + "ref": "prater-amusement-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_prater-amusement-park.jpg" + }, + { + "name": "Vienna Woods Hike and Wine Tasting", + "description": "Escape the city bustle and embark on a scenic hike through the Vienna Woods. Explore lush trails, breathe fresh air, and discover charming villages nestled amidst the rolling hills. Conclude your adventure with a delightful wine tasting at a local vineyard, savoring the region's renowned vintages.", + "locationName": "Vienna Woods", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "vienna", + "ref": "vienna-woods-hike-and-wine-tasting", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_vienna-woods-hike-and-wine-tasting.jpg" + }, + { + "name": "Belvedere Palace Gardens Stroll", + "description": "Immerse yourself in the beauty and tranquility of the Belvedere Palace Gardens. Wander through meticulously manicured landscapes, admire stunning sculptures and fountains, and soak in the serene atmosphere. This is an ideal spot for a relaxing afternoon or a romantic rendezvous.", + "locationName": "Belvedere Palace", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "vienna", + "ref": "belvedere-palace-gardens-stroll", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_belvedere-palace-gardens-stroll.jpg" + }, + { + "name": "Haus des Meeres Aquarium", + "description": "Embark on an underwater journey at Haus des Meeres, Vienna's impressive aquarium housed in a former WWII flak tower. Discover diverse marine life, from sharks and tropical fish to reptiles and birds. The rooftop terrace offers breathtaking city views, making it a unique and educational experience.", + "locationName": "Haus des Meeres", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "vienna", + "ref": "haus-des-meeres-aquarium", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_haus-des-meeres-aquarium.jpg" + }, + { + "name": "Naschmarkt Evening Food Tour", + "description": "Indulge your taste buds on a culinary adventure at Naschmarkt, Vienna's vibrant international market. Join a guided evening food tour to sample diverse flavors from around the world, discover hidden culinary gems, and experience the lively atmosphere of this popular foodie destination.", + "locationName": "Naschmarkt", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "vienna", + "ref": "naschmarkt-evening-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_naschmarkt-evening-food-tour.jpg" + }, + { + "name": "Spanish Riding School Performance", + "description": "Witness the equestrian artistry of the Lipizzaner stallions at the Spanish Riding School, a Viennese institution since the 16th century. Marvel at their graceful movements, intricate formations, and the harmonious bond between horse and rider during a captivating performance in the historic Winter Riding School.", + "locationName": "Spanish Riding School", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "vienna", + "ref": "spanish-riding-school-performance", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_spanish-riding-school-performance.jpg" + }, + { + "name": "Vienna Coffee House Culture", + "description": "Indulge in Vienna's renowned coffee house culture by spending a leisurely afternoon at a traditional café. Savor a cup of Viennese coffee, such as a Wiener Melange or Einspänner, and enjoy a slice of Sachertorte or Apfelstrudel while soaking up the elegant ambiance and observing the local way of life.", + "locationName": "Café Central, Café Sacher, or other traditional coffee houses", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "vienna", + "ref": "vienna-coffee-house-culture", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_vienna-coffee-house-culture.jpg" + }, + { + "name": "Third Man Museum & Sewer Tour", + "description": "Delve into the world of espionage and intrigue with a visit to the Third Man Museum, dedicated to the classic film noir \"The Third Man.\" Explore exhibits on the movie's production and historical context, and embark on a unique tour of Vienna's underground sewer system, as featured in the film's iconic chase scene.", + "locationName": "Third Man Museum", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "vienna", + "ref": "third-man-museum-sewer-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_third-man-museum-sewer-tour.jpg" + }, + { + "name": "Hundertwasserhaus & Kunst Haus Wien", + "description": "Discover the whimsical and colorful architecture of Friedensreich Hundertwasser at the Hundertwasserhaus, a unique apartment complex known for its irregular forms, bright facades, and integration of nature. Afterward, explore the Kunst Haus Wien, a museum showcasing Hundertwasser's artistic vision and ecological philosophies.", + "locationName": "Hundertwasserhaus & Kunst Haus Wien", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "vienna", + "ref": "hundertwasserhaus-kunst-haus-wien", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_hundertwasserhaus-kunst-haus-wien.jpg" + }, + { + "name": "Vienna City Bike Tour", + "description": "Embark on a leisurely bike tour through Vienna's historic streets and picturesque parks. Explore iconic landmarks, hidden gems, and local neighborhoods while enjoying the fresh air and getting some exercise. Guided tours offer insights into the city's history and culture, while self-guided options provide flexibility and freedom.", + "locationName": "Various bike rental shops and tour operators", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "vienna", + "ref": "vienna-city-bike-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_vienna-city-bike-tour.jpg" + }, + { + "name": "Wachau Valley Day Trip", + "description": "Embark on a scenic journey through the picturesque Wachau Valley, a UNESCO World Heritage Site. Discover charming villages, terraced vineyards, and medieval castles as you cruise along the Danube River. Indulge in wine tastings at local wineries, savor regional delicacies, and soak in the breathtaking landscapes.", + "locationName": "Wachau Valley", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "vienna", + "ref": "wachau-valley-day-trip", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_wachau-valley-day-trip.jpg" + }, + { + "name": "Kursalon Vienna Concert", + "description": "Immerse yourself in the enchanting world of Viennese classical music with a captivating concert at the Kursalon Vienna. Delight in the timeless masterpieces of Strauss and Mozart performed by talented musicians in a stunning historic setting. Choose from various concert options and enjoy an unforgettable evening of elegance and culture.", + "locationName": "Kursalon Vienna", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 4, + "destinationRef": "vienna", + "ref": "kursalon-vienna-concert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_kursalon-vienna-concert.jpg" + }, + { + "name": "Central Cemetery Exploration", + "description": "Discover the unique charm of Vienna's Central Cemetery, a sprawling green space that is more than just a burial ground. Explore the ornate tombs of famous figures like Beethoven and Schubert, admire the stunning architecture, and enjoy a peaceful stroll through the serene pathways. Opt for a guided tour to uncover fascinating stories and historical insights.", + "locationName": "Central Cemetery (Zentralfriedhof)", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vienna", + "ref": "central-cemetery-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_central-cemetery-exploration.jpg" + }, + { + "name": "Vienna Rathaus Film Festival", + "description": "Experience the magic of cinema under the stars at the Vienna Rathaus Film Festival. During summer evenings, the square in front of the City Hall transforms into an open-air cinema, showcasing a diverse program of movies, opera performances, and concerts. Enjoy a picnic atmosphere, savor culinary delights from food stalls, and relish a unique cultural experience.", + "locationName": "Vienna City Hall Square", + "duration": 3, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "vienna", + "ref": "vienna-rathaus-film-festival", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_vienna-rathaus-film-festival.jpg" + }, + { + "name": "Grinzing Heuriger Evening", + "description": "Escape the city bustle and venture to Grinzing, a charming village known for its traditional wine taverns called Heuriger. Sample locally produced wines, savor authentic Viennese cuisine, and enjoy live music in a cozy and convivial atmosphere. Experience the authentic Viennese way of life and indulge in the region's rich winemaking heritage.", + "locationName": "Grinzing", + "duration": 4, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "vienna", + "ref": "grinzing-heuriger-evening", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vienna_grinzing-heuriger-evening.jpg" + }, + { + "name": "Explore the Ancient Town", + "description": "Step back in time as you wander through the enchanting streets of Hoi An's Ancient Town, a UNESCO World Heritage Site. Admire the colorful lanterns, traditional wooden houses, and historic temples. Visit the Japanese Covered Bridge, a symbol of the town, and the Fujian Assembly Hall with its intricate carvings. Don't miss the chance to shop for tailor-made clothing and souvenirs at the Central Market.", + "locationName": "Hoi An Ancient Town", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "explore-the-ancient-town", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_explore-the-ancient-town.jpg" + }, + { + "name": "Indulge in a Food Tour", + "description": "Embark on a culinary adventure through Hoi An's vibrant food scene. Join a guided food tour and savor local delicacies like Cao Lau (noodles with pork and greens), White Rose dumplings, and Banh Mi sandwiches. Explore hidden alleyways and discover family-run restaurants, learning about the unique flavors and ingredients of Vietnamese cuisine.", + "locationName": "Various locations in Hoi An", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "vietnam", + "ref": "indulge-in-a-food-tour", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_indulge-in-a-food-tour.jpg" + }, + { + "name": "Cruise the Thu Bon River", + "description": "Enjoy a scenic boat ride along the Thu Bon River, which flows through the heart of Hoi An. Admire the picturesque landscapes, observe local life along the riverbanks, and visit nearby villages known for their traditional crafts. You can choose from various boat options, including traditional wooden boats and modern cruises.", + "locationName": "Thu Bon River", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "cruise-the-thu-bon-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_cruise-the-thu-bon-river.jpg" + }, + { + "name": "Relax on An Bang Beach", + "description": "Escape the bustling town and unwind on the pristine sands of An Bang Beach. Soak up the sun, swim in the crystal-clear waters, or try water sports like surfing and stand-up paddleboarding. Enjoy fresh seafood at beachfront restaurants and witness stunning sunsets over the East Sea.", + "locationName": "An Bang Beach", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "vietnam", + "ref": "relax-on-an-bang-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_relax-on-an-bang-beach.jpg" + }, + { + "name": "Learn the Art of Lantern Making", + "description": "Immerse yourself in Hoi An's cultural heritage by participating in a lantern-making workshop. Learn about the history and significance of lanterns in Vietnamese culture, and create your own unique lantern to take home as a souvenir. This hands-on experience is a great way to connect with local traditions and unleash your creativity.", + "locationName": "Various workshops in Hoi An", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "learn-the-art-of-lantern-making", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_learn-the-art-of-lantern-making.jpg" + }, + { + "name": "Bike through the Countryside", + "description": "Embark on a leisurely bike ride through the scenic Vietnamese countryside surrounding Hoi An. Pedal along rice paddies, quaint villages, and lush greenery, immersing yourself in the peaceful rural atmosphere. This activity allows you to escape the bustling town and experience the authentic charm of the region at your own pace.", + "locationName": "Hoi An Countryside", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "bike-through-the-countryside", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_bike-through-the-countryside.jpg" + }, + { + "name": "Visit My Son Sanctuary", + "description": "Embark on a historical journey to My Son Sanctuary, a UNESCO World Heritage Site. Explore the ruins of ancient Hindu temples dating back to the Champa Kingdom, marveling at the intricate carvings and architectural wonders. Discover the rich history and cultural significance of this once-thriving religious center.", + "locationName": "My Son Sanctuary", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "vietnam", + "ref": "visit-my-son-sanctuary", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_visit-my-son-sanctuary.jpg" + }, + { + "name": "Shop for Custom-Made Clothing", + "description": "Indulge in the renowned tailoring experience of Hoi An. Explore the numerous tailor shops and browse through a vast selection of fabrics and designs. Get measured for a custom-made suit, dress, or any garment of your choice, crafted to your exact specifications. This is a unique opportunity to create a personalized souvenir and embrace the town's tailoring heritage.", + "locationName": "Hoi An Tailor Shops", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "vietnam", + "ref": "shop-for-custom-made-clothing", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_shop-for-custom-made-clothing.jpg" + }, + { + "name": "Take a Cooking Class", + "description": "Delve into the world of Vietnamese cuisine by taking a cooking class. Learn the secrets of preparing traditional dishes, from fresh spring rolls to flavorful pho, under the guidance of experienced local chefs. Discover the art of balancing flavors and using aromatic herbs and spices. This immersive experience allows you to bring home a taste of Vietnam and impress your friends and family with your culinary skills.", + "locationName": "Hoi An Cooking Schools", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "vietnam", + "ref": "take-a-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_take-a-cooking-class.jpg" + }, + { + "name": "Enjoy the Nightlife", + "description": "Experience the vibrant nightlife of Hoi An. Explore the bars and pubs along the riverfront, enjoying live music, refreshing drinks, and a lively atmosphere. Dance the night away at a local club or simply relax and soak up the energetic ambiance. Hoi An offers a diverse nightlife scene catering to various tastes.", + "locationName": "Hoi An Bars and Clubs", + "duration": 3, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "vietnam", + "ref": "enjoy-the-nightlife", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_enjoy-the-nightlife.jpg" + }, + { + "name": "Kayak Through the Nipa Palm Forest", + "description": "Embark on a serene kayaking adventure through the enchanting Nipa Palm Forest. Glide along the tranquil waterways, surrounded by lush greenery and the gentle rustling of palm leaves. Observe local fishermen casting their nets and immerse yourself in the peaceful ambiance of this unique ecosystem.", + "locationName": "Cam Thanh Coconut Village", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "kayak-through-the-nipa-palm-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_kayak-through-the-nipa-palm-forest.jpg" + }, + { + "name": "Discover the Charm of Cam Kim Island", + "description": "Escape the bustling town and venture to Cam Kim Island, a hidden gem just a short ferry ride away. Explore the island's traditional villages, where skilled artisans craft intricate wood carvings and vibrant straw mats. Immerse yourself in the local culture, visit ancient pagodas, and enjoy the peaceful countryside scenery.", + "locationName": "Cam Kim Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "discover-the-charm-of-cam-kim-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_discover-the-charm-of-cam-kim-island.jpg" + }, + { + "name": "Unwind with a Spa Treatment", + "description": "Indulge in a rejuvenating spa experience and let your worries melt away. Hoi An offers a variety of spa treatments, from traditional Vietnamese massages to luxurious body scrubs and facials. Choose from a range of options to suit your needs and emerge feeling refreshed and revitalized.", + "locationName": "Various spas throughout Hoi An", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "vietnam", + "ref": "unwind-with-a-spa-treatment", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_unwind-with-a-spa-treatment.jpg" + }, + { + "name": "Try Your Hand at Basket Boat Riding", + "description": "Experience a unique and thrilling activity by riding a traditional Vietnamese basket boat. Learn how to maneuver these circular boats, used by local fishermen for centuries, and enjoy a fun-filled ride along the Thu Bon River. This is a great way to get a different perspective of the surrounding landscapes and immerse yourself in local culture.", + "locationName": "Thu Bon River", + "duration": 1, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "try-your-hand-at-basket-boat-riding", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_try-your-hand-at-basket-boat-riding.jpg" + }, + { + "name": "Catch a Water Puppet Show", + "description": "Immerse yourself in Vietnamese culture with a captivating water puppet show. Marvel at the intricate puppets as they dance and glide across the water, accompanied by traditional music and storytelling. This unique art form dates back centuries and offers a glimpse into Vietnam's rich cultural heritage.", + "locationName": "Hoi An Theater", + "duration": 1, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "catch-a-water-puppet-show", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_catch-a-water-puppet-show.jpg" + }, + { + "name": "Discover the Marble Mountains", + "description": "Embark on an adventure to the Marble Mountains, a cluster of five marble and limestone hills just south of Hoi An. Explore the natural caves, pagodas, and temples carved into the mountainsides, and enjoy breathtaking panoramic views of the surrounding countryside.", + "locationName": "Marble Mountains", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "discover-the-marble-mountains", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_discover-the-marble-mountains.jpg" + }, + { + "name": "Learn to Surf at An Bang Beach", + "description": "Catch some waves at An Bang Beach, a renowned surfing spot with consistent waves suitable for all levels. Enroll in a surf lesson with experienced instructors and experience the thrill of riding the waves.", + "locationName": "An Bang Beach", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "vietnam", + "ref": "learn-to-surf-at-an-bang-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_learn-to-surf-at-an-bang-beach.jpg" + }, + { + "name": "Visit the Tra Que Vegetable Village", + "description": "Immerse yourself in the local culture at Tra Que Vegetable Village, a charming village known for its organic farming practices. Learn about traditional farming methods, interact with local farmers, and enjoy a delicious farm-to-table meal.", + "locationName": "Tra Que Vegetable Village", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "visit-the-tra-que-vegetable-village", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_visit-the-tra-que-vegetable-village.jpg" + }, + { + "name": "Experience a Traditional Vietnamese Herbal Foot Bath", + "description": "Indulge in a relaxing and rejuvenating experience with a traditional Vietnamese herbal foot bath. Soak your feet in a warm bath infused with local herbs and essential oils, known for their therapeutic properties.", + "locationName": "Various spas and wellness centers", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "experience-a-traditional-vietnamese-herbal-foot-bath", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_experience-a-traditional-vietnamese-herbal-foot-bath.jpg" + }, + { + "name": "Go Birdwatching at the Bay Mau Coconut Forest", + "description": "Embark on a peaceful boat trip through the Bay Mau Coconut Forest, a unique ecosystem home to diverse bird species. Observe the vibrant avian life and immerse yourself in the tranquil beauty of the mangrove forest.", + "locationName": "Bay Mau Coconut Forest", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "vietnam", + "ref": "go-birdwatching-at-the-bay-mau-coconut-forest", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/vietnam_go-birdwatching-at-the-bay-mau-coconut-forest.jpg" + }, + { + "name": "Swim with Whale Sharks at Ningaloo Reef", + "description": "Embark on an unforgettable adventure at Ningaloo Reef, a UNESCO World Heritage Site, and swim alongside gentle giants – whale sharks! These magnificent creatures migrate along the coast of Western Australia between March and August, offering a once-in-a-lifetime experience for snorkelers and divers.", + "locationName": "Ningaloo Reef", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "western-australia", + "ref": "swim-with-whale-sharks-at-ningaloo-reef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_swim-with-whale-sharks-at-ningaloo-reef.jpg" + }, + { + "name": "Explore the Pinnacles Desert", + "description": "Discover the otherworldly landscapes of the Pinnacles Desert in Nambung National Park. Wander through thousands of limestone pillars rising from the golden sand dunes, formed over millions of years. Capture breathtaking photos and enjoy the unique beauty of this natural wonder.", + "locationName": "Nambung National Park", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "western-australia", + "ref": "explore-the-pinnacles-desert", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_explore-the-pinnacles-desert.jpg" + }, + { + "name": "Visit Rottnest Island", + "description": "Escape to Rottnest Island, a car-free paradise just a short ferry ride from Perth. Relax on pristine beaches, cycle around the island, and encounter the adorable quokkas, known as the happiest animals on Earth. Snorkel in crystal-clear waters, explore historic sites, or simply soak up the island vibes.", + "locationName": "Rottnest Island", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "western-australia", + "ref": "visit-rottnest-island", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_visit-rottnest-island.jpg" + }, + { + "name": "Discover the Margaret River Region", + "description": "Indulge in the renowned Margaret River region, famous for its world-class wineries, craft breweries, and gourmet food scene. Embark on a wine tasting tour, savor delicious local produce, and explore the stunning coastline with its dramatic cliffs, pristine beaches, and surf breaks.", + "locationName": "Margaret River", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "western-australia", + "ref": "discover-the-margaret-river-region", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_discover-the-margaret-river-region.jpg" + }, + { + "name": "Venture into the Kimberley", + "description": "Embark on an epic adventure to the Kimberley, a remote and rugged region in the north of Western Australia. Discover ancient gorges, cascading waterfalls, Aboriginal rock art, and unique wildlife. Cruise through Horizontal Falls, hike in the Bungle Bungle Range, and experience the raw beauty of this untamed wilderness.", + "locationName": "Kimberley Region", + "duration": 5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "western-australia", + "ref": "venture-into-the-kimberley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_venture-into-the-kimberley.jpg" + }, + { + "name": "Hike the Bibbulmun Track", + "description": "Embark on an epic hiking adventure along the Bibbulmun Track, one of the world's longest and most renowned long-distance trails. Traverse diverse landscapes, from towering forests and coastal cliffs to rolling hills and tranquil valleys, while encountering unique flora and fauna along the way. Choose from various sections to suit your fitness level and time constraints, and immerse yourself in the natural beauty of Western Australia.", + "locationName": "Bibbulmun Track", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "western-australia", + "ref": "hike-the-bibbulmun-track", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_hike-the-bibbulmun-track.jpg" + }, + { + "name": "Stargaze in the Outback", + "description": "Escape the city lights and venture into the vast outback of Western Australia for an unforgettable stargazing experience. With minimal light pollution, the night sky comes alive with a breathtaking display of stars, constellations, and even the Milky Way. Join a guided astronomy tour or simply lie back and marvel at the celestial wonders above, feeling a sense of awe and wonder in the vastness of the universe.", + "locationName": "Outback Western Australia", + "duration": 4, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "western-australia", + "ref": "stargaze-in-the-outback", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_stargaze-in-the-outback.jpg" + }, + { + "name": "Visit the Monkey Mia Dolphins", + "description": "Experience the magic of interacting with wild dolphins at Monkey Mia, a renowned marine reserve located on the Shark Bay World Heritage Site. Witness these intelligent and playful creatures swim close to shore and participate in the daily feeding sessions, where you can learn about their behavior and conservation efforts. This unique and heartwarming experience is perfect for families and nature enthusiasts alike.", + "locationName": "Monkey Mia", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "western-australia", + "ref": "visit-the-monkey-mia-dolphins", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_visit-the-monkey-mia-dolphins.jpg" + }, + { + "name": "Learn to Surf at Margaret River", + "description": "Catch a wave and experience the thrill of surfing at Margaret River, a world-renowned surfing destination. With consistent swells, pristine beaches, and a laid-back atmosphere, it's the perfect place to learn or improve your surfing skills. Take a lesson from experienced instructors, rent a board, and ride the waves, enjoying the exhilaration and connection with the ocean.", + "locationName": "Margaret River", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": false, + "price": 3, + "destinationRef": "western-australia", + "ref": "learn-to-surf-at-margaret-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_learn-to-surf-at-margaret-river.jpg" + }, + { + "name": "Explore Fremantle's Historic Streets", + "description": "Step back in time and explore the charming port city of Fremantle, known for its rich maritime history and well-preserved architecture. Wander through the historic streets lined with heritage buildings, visit Fremantle Prison, a UNESCO World Heritage Site, and explore the bustling Fremantle Markets, offering local crafts, fresh produce, and unique souvenirs.", + "locationName": "Fremantle", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "western-australia", + "ref": "explore-fremantle-s-historic-streets", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_explore-fremantle-s-historic-streets.jpg" + }, + { + "name": "Kayak with Dolphins in Rockingham", + "description": "Embark on a thrilling kayaking adventure from Rockingham, just south of Perth, where you'll have the opportunity to paddle alongside playful dolphins in their natural habitat. Witness these intelligent creatures up close as they swim, leap, and interact with each other, creating an unforgettable wildlife encounter.", + "locationName": "Rockingham", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "western-australia", + "ref": "kayak-with-dolphins-in-rockingham", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_kayak-with-dolphins-in-rockingham.jpg" + }, + { + "name": "4WD Adventure in the Pilbara", + "description": "Embark on an exhilarating 4WD adventure through the rugged landscapes of the Pilbara region. Explore ancient gorges, towering rock formations, and hidden waterfalls, and discover the rich Aboriginal history and culture of the area.", + "locationName": "Pilbara", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "western-australia", + "ref": "4wd-adventure-in-the-pilbara", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_4wd-adventure-in-the-pilbara.jpg" + }, + { + "name": "Wine Tour in Swan Valley", + "description": "Indulge in a delightful wine tour through the picturesque Swan Valley, Western Australia's oldest wine region. Sample a variety of award-winning wines at charming boutique wineries, savor gourmet local produce, and enjoy the scenic beauty of the vineyards.", + "locationName": "Swan Valley", + "duration": 6, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "western-australia", + "ref": "wine-tour-in-swan-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_wine-tour-in-swan-valley.jpg" + }, + { + "name": "Treetop Walk in Walpole Nornalup National Park", + "description": "Experience a unique perspective of the towering tingle forests with a thrilling treetop walk in Walpole Nornalup National Park. Walk amongst the giants of the forest on a series of suspended walkways and bridges, offering breathtaking views of the canopy and the surrounding landscape.", + "locationName": "Walpole Nornalup National Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "western-australia", + "ref": "treetop-walk-in-walpole-nornalup-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_treetop-walk-in-walpole-nornalup-national-park.jpg" + }, + { + "name": "Catch a Sunset at Cable Beach", + "description": "Witness the magical spectacle of a Western Australian sunset at the iconic Cable Beach in Broome. Relax on the pristine white sand, watch the sun dip below the horizon, casting vibrant hues across the sky, and perhaps even enjoy a camel ride along the beach for a truly unforgettable experience.", + "locationName": "Cable Beach, Broome", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "western-australia", + "ref": "catch-a-sunset-at-cable-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_catch-a-sunset-at-cable-beach.jpg" + }, + { + "name": "Go on a wildlife cruise to spot humpback whales", + "description": "Embark on an unforgettable whale-watching cruise from Augusta or Dunsborough between June and December. Witness the majestic humpback whales as they migrate along the coast, breaching and playing in the pristine waters. This is a breathtaking experience for nature enthusiasts and families alike.", + "locationName": "Augusta or Dunsborough", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "western-australia", + "ref": "go-on-a-wildlife-cruise-to-spot-humpback-whales", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_go-on-a-wildlife-cruise-to-spot-humpback-whales.jpg" + }, + { + "name": "Explore the ancient landscapes of Karijini National Park", + "description": "Discover the rugged beauty of Karijini National Park, home to breathtaking gorges, cascading waterfalls, and ancient rock formations. Hike through the park's trails, take a refreshing dip in the natural pools, and marvel at the stunning scenery. This adventure is perfect for outdoor enthusiasts seeking a unique experience.", + "locationName": "Karijini National Park", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "western-australia", + "ref": "explore-the-ancient-landscapes-of-karijini-national-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_explore-the-ancient-landscapes-of-karijini-national-park.jpg" + }, + { + "name": "Visit the world's largest fringing reef, Ningaloo Reef", + "description": "Dive into the turquoise waters of Ningaloo Reef, the world's largest fringing reef, and encounter an abundance of marine life. Snorkel or scuba dive amongst colorful coral reefs, swim with manta rays, and witness the gentle giants of the ocean – whale sharks.", + "locationName": "Ningaloo Reef", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "western-australia", + "ref": "visit-the-world-s-largest-fringing-reef-ningaloo-reef", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_visit-the-world-s-largest-fringing-reef-ningaloo-reef.jpg" + }, + { + "name": "Delve into Aboriginal culture and history", + "description": "Immerse yourself in the rich culture and history of Australia's Aboriginal people through guided tours and experiences. Learn about their ancient traditions, connection to the land, and unique art forms. This is a meaningful and educational experience for travelers seeking a deeper understanding of Australia's heritage.", + "locationName": "Various locations across Western Australia", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "western-australia", + "ref": "delve-into-aboriginal-culture-and-history", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_delve-into-aboriginal-culture-and-history.jpg" + }, + { + "name": "Indulge in a gourmet food and wine experience", + "description": "Western Australia boasts a thriving food and wine scene. Embark on a culinary journey, sampling fresh local produce, award-winning wines, and craft beers. Visit renowned restaurants, charming cafes, and vibrant farmers' markets to savor the flavors of the region.", + "locationName": "Margaret River, Swan Valley, Perth", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "western-australia", + "ref": "indulge-in-a-gourmet-food-and-wine-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/western-australia_indulge-in-a-gourmet-food-and-wine-experience.jpg" + }, + { + "name": "Witness the Eruption of Old Faithful", + "description": "Experience the iconic eruption of Old Faithful, a world-renowned geyser that shoots boiling water high into the air at regular intervals. Witnessing this natural wonder is a must-do for any visitor to Yellowstone.", + "locationName": "Upper Geyser Basin", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "yellowstone-national-park", + "ref": "witness-the-eruption-of-old-faithful", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_witness-the-eruption-of-old-faithful.jpg" + }, + { + "name": "Wildlife Watching in Lamar Valley", + "description": "Embark on a wildlife safari in Lamar Valley, known as the 'Serengeti of North America.' Spot herds of bison, elk, and pronghorn, and with some luck, you might even see wolves, bears, or coyotes. Bring your binoculars and camera for an unforgettable experience.", + "locationName": "Lamar Valley", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "yellowstone-national-park", + "ref": "wildlife-watching-in-lamar-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_wildlife-watching-in-lamar-valley.jpg" + }, + { + "name": "Hike to the Grand Prismatic Spring", + "description": "Embark on a scenic hike to the Grand Prismatic Spring, the largest hot spring in the United States. Marvel at the vibrant colors of the spring, caused by thermophilic bacteria, and enjoy panoramic views of the surrounding geothermal landscape.", + "locationName": "Midway Geyser Basin", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "yellowstone-national-park", + "ref": "hike-to-the-grand-prismatic-spring", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_hike-to-the-grand-prismatic-spring.jpg" + }, + { + "name": "Explore the Mud Volcano Area", + "description": "Discover the fascinating geothermal features of the Mud Volcano Area, where bubbling mudpots and fumaroles create a unique and otherworldly landscape. Learn about the volcanic processes that shape this area and witness the power of nature up close.", + "locationName": "Mud Volcano Area", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "yellowstone-national-park", + "ref": "explore-the-mud-volcano-area", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_explore-the-mud-volcano-area.jpg" + }, + { + "name": "Take a Scenic Drive along the Grand Loop Road", + "description": "Embark on a scenic drive along the Grand Loop Road, a 142-mile route that circles the park and offers stunning views of mountains, lakes, geysers, and canyons. Stop at various viewpoints and visitor centers along the way to learn about the park's history and geology.", + "locationName": "Grand Loop Road", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "yellowstone-national-park", + "ref": "take-a-scenic-drive-along-the-grand-loop-road", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_take-a-scenic-drive-along-the-grand-loop-road.jpg" + }, + { + "name": "Go horseback riding through scenic trails", + "description": "Experience the beauty of Yellowstone National Park from a unique perspective on a guided horseback riding tour. Several outfitters offer various trail options, catering to different skill levels and interests. Whether you're a seasoned rider or a beginner, you'll enjoy traversing meadows, forests, and mountain vistas while immersing yourself in the park's serene landscapes. Keep an eye out for wildlife sightings along the way, making your horseback adventure even more memorable.", + "locationName": "Various locations within Yellowstone National Park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "yellowstone-national-park", + "ref": "go-horseback-riding-through-scenic-trails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_go-horseback-riding-through-scenic-trails.jpg" + }, + { + "name": "Fly fishing in pristine rivers and streams", + "description": "Yellowstone National Park is a haven for fly fishing enthusiasts, boasting renowned rivers and streams teeming with trout. Cast your line in the crystal-clear waters of the Yellowstone River, the Madison River, or the Firehole River, surrounded by stunning natural beauty. Whether you're a seasoned angler or a novice, the park offers ample opportunities to test your skills and experience the thrill of catching wild trout.", + "locationName": "Yellowstone River, Madison River, Firehole River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yellowstone-national-park", + "ref": "fly-fishing-in-pristine-rivers-and-streams", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_fly-fishing-in-pristine-rivers-and-streams.jpg" + }, + { + "name": "Camp under the stars in designated campgrounds", + "description": "Immerse yourself in the wilderness of Yellowstone National Park by camping under the starry night sky. The park offers numerous campgrounds, each providing a unique experience. Fall asleep to the sounds of nature and wake up to breathtaking views. Whether you prefer a developed campground with amenities or a more primitive backcountry site, camping in Yellowstone is an unforgettable way to connect with the natural world.", + "locationName": "Various campgrounds within Yellowstone National Park", + "duration": 12, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "yellowstone-national-park", + "ref": "camp-under-the-stars-in-designated-campgrounds", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_camp-under-the-stars-in-designated-campgrounds.jpg" + }, + { + "name": "Take a boat tour on Yellowstone Lake", + "description": "Embark on a scenic boat tour across the pristine waters of Yellowstone Lake, the largest high-altitude lake in North America. Enjoy breathtaking views of the surrounding mountains and forests while learning about the lake's history, geology, and ecology. Keep an eye out for wildlife such as eagles, ospreys, and waterfowl. Some tours even offer opportunities for fishing or swimming.", + "locationName": "Yellowstone Lake", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yellowstone-national-park", + "ref": "take-a-boat-tour-on-yellowstone-lake", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_take-a-boat-tour-on-yellowstone-lake.jpg" + }, + { + "name": "Visit the historic Old Faithful Inn", + "description": "Step back in time at the iconic Old Faithful Inn, a National Historic Landmark renowned for its rustic architecture and grandeur. Explore the inn's unique features, including the massive stone fireplace, the handcrafted log furniture, and the impressive seven-story lobby. Take a guided tour to learn about the inn's history and construction, or simply relax and soak up the atmosphere of this architectural marvel.", + "locationName": "Old Faithful Inn", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yellowstone-national-park", + "ref": "visit-the-historic-old-faithful-inn", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_visit-the-historic-old-faithful-inn.jpg" + }, + { + "name": "Go Whitewater Rafting on the Yellowstone River", + "description": "Experience the thrill of whitewater rafting on the Yellowstone River. Navigate through exhilarating rapids, surrounded by stunning canyon scenery and the possibility of spotting wildlife along the riverbanks. Several outfitters offer guided tours for various skill levels, ensuring a safe and unforgettable adventure.", + "locationName": "Yellowstone River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "yellowstone-national-park", + "ref": "go-whitewater-rafting-on-the-yellowstone-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_go-whitewater-rafting-on-the-yellowstone-river.jpg" + }, + { + "name": "Visit the Grizzly & Wolf Discovery Center", + "description": "Get up close and personal with majestic grizzly bears and gray wolves at the Grizzly & Wolf Discovery Center in West Yellowstone. Learn about these fascinating creatures through educational exhibits and observe their behaviors in spacious enclosures. This experience offers a unique opportunity to understand and appreciate these iconic animals.", + "locationName": "Grizzly & Wolf Discovery Center", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yellowstone-national-park", + "ref": "visit-the-grizzly-wolf-discovery-center", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_visit-the-grizzly-wolf-discovery-center.jpg" + }, + { + "name": "Stargazing in the World's First Dark Sky Park", + "description": "Escape the city lights and marvel at the breathtaking night sky in Yellowstone, designated as the world's first International Dark Sky Park. Join a ranger-led stargazing program or simply find a secluded spot to witness the Milky Way, constellations, and celestial wonders with exceptional clarity.", + "locationName": "Various locations throughout the park", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "yellowstone-national-park", + "ref": "stargazing-in-the-world-s-first-dark-sky-park", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_stargazing-in-the-world-s-first-dark-sky-park.jpg" + }, + { + "name": "Explore the Historic Fort Yellowstone", + "description": "Step back in time and explore the historic Fort Yellowstone, established in the late 19th century to protect the park. Visit the preserved buildings, including the former army barracks and officer's quarters, and learn about the early history of Yellowstone and the role of the U.S. Army in its conservation.", + "locationName": "Fort Yellowstone", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "yellowstone-national-park", + "ref": "explore-the-historic-fort-yellowstone", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_explore-the-historic-fort-yellowstone.jpg" + }, + { + "name": "Take a Dip in the Boiling River", + "description": "Experience a unique geothermal phenomenon at the Boiling River, where a hot spring flows into the Gardner River, creating a natural hot tub. Relax and soak in the warm waters while enjoying the surrounding scenery. Be sure to follow safety guidelines and check for closures before visiting.", + "locationName": "Boiling River", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "yellowstone-national-park", + "ref": "take-a-dip-in-the-boiling-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_take-a-dip-in-the-boiling-river.jpg" + }, + { + "name": "Snowshoeing or Cross-Country Skiing in Winter Wonderland", + "description": "Experience the magic of Yellowstone blanketed in snow with a peaceful snowshoeing or cross-country skiing adventure. Glide through serene meadows and snow-covered forests, surrounded by breathtaking winter scenery. Numerous trails cater to different skill levels, making it a perfect winter activity for families and individuals seeking tranquility in nature.", + "locationName": "Various trails throughout the park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yellowstone-national-park", + "ref": "snowshoeing-or-cross-country-skiing-in-winter-wonderland", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_snowshoeing-or-cross-country-skiing-in-winter-wonderland.jpg" + }, + { + "name": "Photography Tour of Yellowstone's Hidden Gems", + "description": "Embark on a photography tour led by a local expert to capture the essence of Yellowstone's lesser-known wonders. Discover hidden waterfalls, secluded geothermal areas, and picturesque landscapes, while learning photography techniques to immortalize the park's beauty. This tour is ideal for photography enthusiasts and those seeking unique perspectives of Yellowstone.", + "locationName": "Various off-the-beaten-path locations", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "yellowstone-national-park", + "ref": "photography-tour-of-yellowstone-s-hidden-gems", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_photography-tour-of-yellowstone-s-hidden-gems.jpg" + }, + { + "name": "Attend a Ranger-led Program", + "description": "Engage in a ranger-led program to delve deeper into Yellowstone's ecosystem, history, and geology. Park rangers offer a variety of informative and entertaining programs throughout the day, including guided walks, campfire talks, and evening presentations. These programs provide valuable insights and enhance your understanding of the park's natural and cultural significance.", + "locationName": "Visitor centers and various locations throughout the park", + "duration": 1, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "yellowstone-national-park", + "ref": "attend-a-ranger-led-program", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_attend-a-ranger-led-program.jpg" + }, + { + "name": "Visit the Museum of the National Park Ranger", + "description": "Explore the Museum of the National Park Ranger to discover the history and heritage of park rangers. Learn about their vital role in preserving national parks and the challenges they face. The museum offers exhibits, artifacts, and interactive displays, providing a unique perspective on the park ranger profession.", + "locationName": "Norris Geyser Basin", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "yellowstone-national-park", + "ref": "visit-the-museum-of-the-national-park-ranger", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_visit-the-museum-of-the-national-park-ranger.jpg" + }, + { + "name": "Biking on Designated Trails", + "description": "Enjoy a scenic bike ride along Yellowstone's designated trails. Explore different areas of the park at your own pace, immersing yourself in the surrounding nature. Several trails offer stunning views and opportunities for wildlife sightings. Biking is a fantastic way to experience the park's vastness while getting some exercise.", + "locationName": "Various paved and unpaved trails throughout the park", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yellowstone-national-park", + "ref": "biking-on-designated-trails", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yellowstone-national-park_biking-on-designated-trails.jpg" + }, + { + "name": "Hiking to Yosemite Falls", + "description": "Embark on an unforgettable hike to the top of Yosemite Falls, North America's tallest waterfall. Witness breathtaking views of the valley and surrounding granite cliffs as you ascend through lush forests and rocky terrain.", + "locationName": "Yosemite Valley", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "yosemite-national-park", + "ref": "hiking-to-yosemite-falls", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_hiking-to-yosemite-falls.jpg" + }, + { + "name": "Stargazing in Glacier Point", + "description": "Experience the magic of Yosemite's night sky at Glacier Point. Away from city lights, marvel at the Milky Way and countless stars, creating a truly unforgettable celestial experience.", + "locationName": "Glacier Point", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": true, + "price": 1, + "destinationRef": "yosemite-national-park", + "ref": "stargazing-in-glacier-point", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_stargazing-in-glacier-point.jpg" + }, + { + "name": "Rock Climbing El Capitan", + "description": "Challenge yourself with a thrilling rock climbing adventure on El Capitan, one of the world's most iconic rock formations. With various routes for different skill levels, experience the adrenaline rush and stunning views from the top.", + "locationName": "El Capitan", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "yosemite-national-park", + "ref": "rock-climbing-el-capitan", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_rock-climbing-el-capitan.jpg" + }, + { + "name": "Exploring the Mariposa Grove of Giant Sequoias", + "description": "Wander among the majestic giants of Mariposa Grove, home to some of the world's largest and oldest living trees. Walk through the Grizzly Giant Loop Trail and witness the awe-inspiring size and beauty of these ancient sequoias.", + "locationName": "Mariposa Grove", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yosemite-national-park", + "ref": "exploring-the-mariposa-grove-of-giant-sequoias", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_exploring-the-mariposa-grove-of-giant-sequoias.jpg" + }, + { + "name": "Scenic Drive along Tioga Road", + "description": "Embark on a scenic drive along Tioga Road, offering breathtaking views of Yosemite's high country. Pass by alpine meadows, granite domes, and pristine lakes, stopping at lookout points to capture the stunning vistas.", + "locationName": "Tioga Road", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "yosemite-national-park", + "ref": "scenic-drive-along-tioga-road", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_scenic-drive-along-tioga-road.jpg" + }, + { + "name": "Whitewater Rafting on the Merced River", + "description": "Experience the thrill of whitewater rafting on the Merced River, navigating through exhilarating rapids surrounded by the stunning scenery of Yosemite Valley. Several outfitters offer guided tours for various skill levels, making it an unforgettable adventure for families and thrill-seekers alike.", + "locationName": "Merced River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "yosemite-national-park", + "ref": "whitewater-rafting-on-the-merced-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_whitewater-rafting-on-the-merced-river.jpg" + }, + { + "name": "Biking Through Yosemite Valley", + "description": "Rent a bike and explore the paved paths winding through Yosemite Valley, offering a leisurely way to soak in the breathtaking views of El Capitan, Half Dome, and Yosemite Falls. It's a fantastic option for families with children or those who prefer a more relaxed pace to experience the park's beauty.", + "locationName": "Yosemite Valley", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yosemite-national-park", + "ref": "biking-through-yosemite-valley", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_biking-through-yosemite-valley.jpg" + }, + { + "name": "Photography Tour of Yosemite's Icons", + "description": "Join a photography tour led by a local expert to capture the essence of Yosemite's iconic landmarks. Learn about composition, lighting, and techniques to create stunning images of Half Dome, El Capitan, Yosemite Falls, and the surrounding landscapes. This tour is perfect for photography enthusiasts of all levels.", + "locationName": "Various locations throughout Yosemite Valley", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "yosemite-national-park", + "ref": "photography-tour-of-yosemite-s-icons", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_photography-tour-of-yosemite-s-icons.jpg" + }, + { + "name": "Horseback Riding in the High Sierra", + "description": "Embark on a horseback riding adventure through the scenic trails of the High Sierra. Several stables offer guided tours, allowing you to explore the backcountry, meadows, and forests while enjoying the tranquility of nature and the company of these gentle animals.", + "locationName": "High Sierra Camps or Yosemite Valley stables", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "yosemite-national-park", + "ref": "horseback-riding-in-the-high-sierra", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_horseback-riding-in-the-high-sierra.jpg" + }, + { + "name": "Birdwatching in Yosemite's Diverse Habitats", + "description": "With over 260 bird species recorded in Yosemite, birdwatching is a rewarding activity for nature enthusiasts. Explore various habitats, from meadows and forests to rivers and cliffs, and spot a variety of birds, including Steller's jays, woodpeckers, and even bald eagles.", + "locationName": "Various locations throughout the park", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 1, + "destinationRef": "yosemite-national-park", + "ref": "birdwatching-in-yosemite-s-diverse-habitats", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_birdwatching-in-yosemite-s-diverse-habitats.jpg" + }, + { + "name": "Swimming in the Merced River", + "description": "Cool off with a refreshing dip in the Merced River! Several swimming holes are scattered throughout the park, offering stunning views and a chance to relax in nature. Popular spots include Sentinel Beach and Cathedral Beach. Remember to check the water conditions before swimming and be aware of potential currents.", + "locationName": "Merced River", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 1, + "destinationRef": "yosemite-national-park", + "ref": "swimming-in-the-merced-river", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_swimming-in-the-merced-river.jpg" + }, + { + "name": "Visiting the Yosemite Museum", + "description": "Delve into the rich history and culture of Yosemite Valley at the Yosemite Museum. Discover exhibits on the Ahwahneechee people, the park's geological formations, and the early pioneers who explored the area. Learn about the delicate balance of nature and the importance of conservation efforts.", + "locationName": "Yosemite Valley", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yosemite-national-park", + "ref": "visiting-the-yosemite-museum", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_visiting-the-yosemite-museum.jpg" + }, + { + "name": "Art Classes and Workshops", + "description": "Unleash your creativity amidst the inspiring landscapes of Yosemite! The park offers various art classes and workshops, including painting, photography, and sketching. Capture the beauty of your surroundings on canvas or learn new techniques from experienced instructors.", + "locationName": "Various locations throughout the park", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "yosemite-national-park", + "ref": "art-classes-and-workshops", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_art-classes-and-workshops.jpg" + }, + { + "name": "Ranger-Led Programs", + "description": "Join knowledgeable park rangers for informative and engaging programs that delve deeper into Yosemite's wonders. Participate in guided nature walks, campfire talks, and evening presentations. Learn about the park's flora and fauna, its geological history, and ongoing conservation efforts.", + "locationName": "Various locations throughout the park", + "duration": 1.5, + "timeOfDay": "any", + "familyFriendly": true, + "price": 1, + "destinationRef": "yosemite-national-park", + "ref": "ranger-led-programs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_ranger-led-programs.jpg" + }, + { + "name": "Fine Dining at The Ahwahnee", + "description": "Indulge in a memorable dining experience at The Ahwahnee, a historic hotel renowned for its elegant ambiance and exquisite cuisine. Savor delicious meals prepared with fresh, local ingredients while enjoying breathtaking views of Yosemite Valley. Reservations are recommended.", + "locationName": "The Ahwahnee Hotel", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": false, + "price": 4, + "destinationRef": "yosemite-national-park", + "ref": "fine-dining-at-the-ahwahnee", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_fine-dining-at-the-ahwahnee.jpg" + }, + { + "name": "Backpacking in the Yosemite Wilderness", + "description": "Embark on a multi-day backpacking adventure into the heart of Yosemite's wilderness. Hike through pristine landscapes, camp under the stars, and experience the park's raw beauty far from the crowds. Choose from various trails like the John Muir Trail or the High Sierra Camps Loop, offering stunning views and a true escape into nature.", + "locationName": "Yosemite Wilderness", + "duration": 24, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "yosemite-national-park", + "ref": "backpacking-in-the-yosemite-wilderness", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_backpacking-in-the-yosemite-wilderness.jpg" + }, + { + "name": "Fishing in Yosemite's Lakes and Streams", + "description": "Cast a line and enjoy a peaceful day of fishing in Yosemite's pristine lakes and streams. Mirror Lake, Tenaya Lake, and the Merced River offer opportunities to catch trout, bass, and other fish species. Obtain a California fishing license and savor the tranquility of nature while waiting for a bite.", + "locationName": "Mirror Lake, Tenaya Lake, Merced River", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yosemite-national-park", + "ref": "fishing-in-yosemite-s-lakes-and-streams", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_fishing-in-yosemite-s-lakes-and-streams.jpg" + }, + { + "name": "Winter Sports in Yosemite", + "description": "Experience the magic of Yosemite in winter with a variety of snow activities. Badger Pass Ski Area offers downhill skiing, snowboarding, and snow tubing for all skill levels. Cross-country skiing and snowshoeing trails wind through the park's winter wonderland, providing breathtaking views and a serene escape.", + "locationName": "Badger Pass Ski Area", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "yosemite-national-park", + "ref": "winter-sports-in-yosemite", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_winter-sports-in-yosemite.jpg" + }, + { + "name": "Exploring Yosemite Valley by Bike", + "description": "Rent a bike and explore the wonders of Yosemite Valley on two wheels. Cycle along paved paths, enjoying iconic views of Half Dome, El Capitan, and Yosemite Falls. Stop at various points of interest, have a picnic by the Merced River, and experience the valley's beauty at your own pace.", + "locationName": "Yosemite Valley", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yosemite-national-park", + "ref": "exploring-yosemite-valley-by-bike", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_exploring-yosemite-valley-by-bike.jpg" + }, + { + "name": "Rock Climbing Lessons and Guided Climbs", + "description": "Challenge yourself with rock climbing in Yosemite, a world-renowned destination for this thrilling activity. Take lessons from experienced guides to learn the basics or join a guided climb to conquer iconic formations like El Capitan or Half Dome. Experience the adrenaline rush and breathtaking views from the top.", + "locationName": "El Capitan, Half Dome", + "duration": 8, + "timeOfDay": "any", + "familyFriendly": false, + "price": 4, + "destinationRef": "yosemite-national-park", + "ref": "rock-climbing-lessons-and-guided-climbs", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yosemite-national-park_rock-climbing-lessons-and-guided-climbs.jpg" + }, + { + "name": "Explore Chichen Itza", + "description": "Embark on a journey through time at Chichen Itza, the awe-inspiring ancient Mayan city. Marvel at the iconic El Castillo pyramid, the Great Ball Court, and the Temple of Warriors. Uncover the mysteries of Mayan astronomy at the observatory and immerse yourself in the rich history and culture of this UNESCO World Heritage Site.", + "locationName": "Chichen Itza", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "yucatan-peninsula", + "ref": "explore-chichen-itza", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_explore-chichen-itza.jpg" + }, + { + "name": "Dive into Cenotes", + "description": "Discover the hidden underwater world of the Yucatan Peninsula's cenotes. Snorkel or scuba dive in these natural sinkholes filled with crystal-clear freshwater, exploring stunning stalactites, stalagmites, and unique marine life. Dos Ojos Cenote and Gran Cenote offer unforgettable experiences for all levels of divers.", + "locationName": "Various Cenotes (Dos Ojos, Gran Cenote)", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yucatan-peninsula", + "ref": "dive-into-cenotes", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_dive-into-cenotes.jpg" + }, + { + "name": "Relax on Tulum's Beaches", + "description": "Unwind on the pristine white sand beaches of Tulum, where turquoise waters meet ancient Mayan ruins. Soak up the sun, swim in the Caribbean Sea, and enjoy breathtaking views of the Tulum Archaeological Site perched on a clifftop. Explore nearby beach clubs and indulge in delicious local cuisine.", + "locationName": "Tulum Beaches", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yucatan-peninsula", + "ref": "relax-on-tulum-s-beaches", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_relax-on-tulum-s-beaches.jpg" + }, + { + "name": "Swim with Whale Sharks", + "description": "Embark on an unforgettable adventure swimming alongside gentle giants, the whale sharks. From May to September, these magnificent creatures gather off the coast of Isla Mujeres and Holbox. Join a responsible tour and experience the thrill of snorkeling or diving near these majestic animals in their natural habitat.", + "locationName": "Isla Mujeres or Holbox", + "duration": 6, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "yucatan-peninsula", + "ref": "swim-with-whale-sharks", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_swim-with-whale-sharks.jpg" + }, + { + "name": "Explore Valladolid", + "description": "Step back in time in the charming colonial city of Valladolid. Stroll through its colorful streets, admire the historic architecture, and visit the Convent of San Bernardino of Siena. Discover the vibrant local culture at the Mercado Municipal, where you can find handcrafted souvenirs and delicious Yucatecan cuisine.", + "locationName": "Valladolid", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 2, + "destinationRef": "yucatan-peninsula", + "ref": "explore-valladolid", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_explore-valladolid.jpg" + }, + { + "name": "Kayaking through Sian Ka'an Biosphere Reserve", + "description": "Embark on a serene kayaking adventure through the Sian Ka'an Biosphere Reserve, a UNESCO World Heritage Site. Paddle through mangrove forests, spot diverse wildlife like dolphins, turtles, and exotic birds, and immerse yourself in the natural beauty of this protected ecosystem. Choose from guided tours or rent kayaks for a self-guided exploration.", + "locationName": "Sian Ka'an Biosphere Reserve", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "yucatan-peninsula", + "ref": "kayaking-through-sian-ka-an-biosphere-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_kayaking-through-sian-ka-an-biosphere-reserve.jpg" + }, + { + "name": "Visit the Yellow City of Izamal", + "description": "Step into a world of vibrant yellow hues in the charming colonial town of Izamal. Explore the Convent of San Antonio de Padua, a historic monastery with a vast atrium, and climb the Kinich Kak Moo pyramid for panoramic views. Discover local artisan shops, indulge in Yucatecan cuisine, and experience the tranquil atmosphere of this unique town.", + "locationName": "Izamal", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yucatan-peninsula", + "ref": "visit-the-yellow-city-of-izamal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_visit-the-yellow-city-of-izamal.jpg" + }, + { + "name": "Sample Tequila and Mezcal at a Hacienda", + "description": "Delve into the world of Mexican spirits with a tequila and mezcal tasting at a traditional hacienda. Learn about the production process, discover the distinct flavors of different varieties, and savor the unique aromas. Many haciendas offer tours that showcase the history and culture of the region, providing a glimpse into the Yucatan's past.", + "locationName": "Various haciendas throughout Yucatan", + "duration": 2, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "yucatan-peninsula", + "ref": "sample-tequila-and-mezcal-at-a-hacienda", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_sample-tequila-and-mezcal-at-a-hacienda.jpg" + }, + { + "name": "Take a Cooking Class and Learn Yucatecan Cuisine", + "description": "Unleash your inner chef and learn the secrets of Yucatecan cuisine with a hands-on cooking class. Master traditional dishes like cochinita pibil, sopa de lima, and panuchos, using fresh local ingredients. Immerse yourself in the flavors and techniques of this unique culinary tradition, and take home newfound skills to recreate the magic in your own kitchen.", + "locationName": "Various cooking schools and restaurants in Yucatan", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "yucatan-peninsula", + "ref": "take-a-cooking-class-and-learn-yucatecan-cuisine", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_take-a-cooking-class-and-learn-yucatecan-cuisine.jpg" + }, + { + "name": "Go Birdwatching in Celestun Biosphere Reserve", + "description": "Embark on a birdwatching adventure in the Celestun Biosphere Reserve, home to a diverse array of avian species. Observe pink flamingos in their natural habitat, spot herons, pelicans, and other migratory birds. Take a boat tour through the mangroves or explore the reserve's walking trails for an unforgettable experience.", + "locationName": "Celestun Biosphere Reserve", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "yucatan-peninsula", + "ref": "go-birdwatching-in-celestun-biosphere-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_go-birdwatching-in-celestun-biosphere-reserve.jpg" + }, + { + "name": "Sunset Sail in the Caribbean Sea", + "description": "Embark on a magical sunset cruise along the turquoise waters of the Caribbean Sea. As the sun dips below the horizon, casting hues of orange and pink across the sky, enjoy breathtaking views of the coastline and the gentle rocking of the boat. Savor delicious cocktails and snacks while keeping an eye out for playful dolphins or sea turtles that might grace you with their presence. This romantic and relaxing experience is perfect for couples or anyone seeking a moment of tranquility.", + "locationName": "Caribbean Sea", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "yucatan-peninsula", + "ref": "sunset-sail-in-the-caribbean-sea", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_sunset-sail-in-the-caribbean-sea.jpg" + }, + { + "name": "Explore the Pink Lakes of Las Coloradas", + "description": "Venture to the unique pink lakes of Las Coloradas, a natural wonder that will leave you awestruck. These vibrant pink waters get their color from salt-loving microorganisms and create a surreal landscape, perfect for capturing stunning photos. Learn about the salt production process at the nearby salt factory and take a dip in the refreshing waters for a truly unforgettable experience. ", + "locationName": "Las Coloradas", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yucatan-peninsula", + "ref": "explore-the-pink-lakes-of-las-coloradas", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_explore-the-pink-lakes-of-las-coloradas.jpg" + }, + { + "name": "Discover Ek Balam and Rappel into a Cenote", + "description": "Combine history and adventure with a visit to the ancient Mayan city of Ek Balam. Climb the Acropolis pyramid for panoramic views of the surrounding jungle, and then descend into the depths of a cenote by rappelling down its limestone walls. This thrilling experience allows you to explore the hidden world of these natural sinkholes and cool off in their refreshing waters. ", + "locationName": "Ek Balam", + "duration": 5, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "yucatan-peninsula", + "ref": "discover-ek-balam-and-rappel-into-a-cenote", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_discover-ek-balam-and-rappel-into-a-cenote.jpg" + }, + { + "name": "Shop for Handicrafts in Merida's Markets", + "description": "Immerse yourself in the vibrant culture of Merida by exploring its bustling markets. Discover unique handcrafted souvenirs, including textiles, ceramics, and jewelry, all made by local artisans. Wander through the Mercado Lucas de Galvez, the largest market in the city, and soak up the lively atmosphere. This is a great opportunity to find authentic treasures and support the local economy.", + "locationName": "Merida", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "yucatan-peninsula", + "ref": "shop-for-handicrafts-in-merida-s-markets", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_shop-for-handicrafts-in-merida-s-markets.jpg" + }, + { + "name": "Horseback Riding through the Jungle", + "description": "Embark on an adventurous horseback riding tour through the lush Yucatan jungle. Follow scenic trails, passing by ancient Mayan ruins and hidden cenotes, and admire the diverse flora and fauna. This is a unique way to experience the natural beauty of the region and create lasting memories. Whether you're an experienced rider or a beginner, this activity offers a thrilling and unforgettable adventure.", + "locationName": "Yucatan Jungle", + "duration": 2, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "yucatan-peninsula", + "ref": "horseback-riding-through-the-jungle", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_horseback-riding-through-the-jungle.jpg" + }, + { + "name": "Uncover the Secrets of Coba", + "description": "Embark on a journey to Coba, an ancient Mayan city nestled deep in the jungle. Climb Nohoch Mul, the tallest pyramid in the Yucatan, and enjoy breathtaking panoramic views of the surrounding rainforest. Explore the ancient ball court, temples, and sacbes (white roads) that once connected this bustling city. Rent a bike or hire a bicitaxi to navigate the vast archaeological site and truly immerse yourself in the Mayan world.", + "locationName": "Coba", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "yucatan-peninsula", + "ref": "uncover-the-secrets-of-coba", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_uncover-the-secrets-of-coba.jpg" + }, + { + "name": "Take a Salsa Dancing Class", + "description": "Immerse yourself in the vibrant nightlife of the Yucatan Peninsula by taking a salsa dancing class. Learn the basic steps and rhythms of this energetic Latin dance, and then hit the dance floor to practice your newfound skills. Many bars and clubs offer salsa nights, providing the perfect opportunity to show off your moves and mingle with locals.", + "locationName": "Merida or Playa del Carmen", + "duration": 2, + "timeOfDay": "night", + "familyFriendly": false, + "price": 2, + "destinationRef": "yucatan-peninsula", + "ref": "take-a-salsa-dancing-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_take-a-salsa-dancing-class.jpg" + }, + { + "name": "Indulge in a Temazcal Ceremony", + "description": "Experience a traditional Mayan temazcal ceremony, a purifying ritual that combines heat, steam, and medicinal herbs. Enter a dome-shaped sweat lodge led by a shaman and participate in chanting, meditation, and cleansing rituals. This unique cultural experience is believed to promote physical and spiritual well-being.", + "locationName": "Various locations throughout the Yucatan", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "yucatan-peninsula", + "ref": "indulge-in-a-temazcal-ceremony", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_indulge-in-a-temazcal-ceremony.jpg" + }, + { + "name": "Go Spelunking in Rio Secreto", + "description": "Embark on an unforgettable underground adventure at Rio Secreto, a stunning network of underground rivers and caves. Swim and wade through crystal-clear waters, marvel at dramatic stalactites and stalagmites, and learn about the geological history of the region. This unique experience offers a glimpse into the hidden world beneath the Yucatan's surface.", + "locationName": "Rio Secreto", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 4, + "destinationRef": "yucatan-peninsula", + "ref": "go-spelunking-in-rio-secreto", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_go-spelunking-in-rio-secreto.jpg" + }, + { + "name": "Explore the Ruins of Uxmal", + "description": "Journey back in time at Uxmal, another impressive Mayan archaeological site renowned for its Puuc architectural style. Admire the intricate carvings on the Pyramid of the Magician, the Governor's Palace, and the Nunnery Quadrangle. Learn about the fascinating history and mythology of the Mayan civilization as you explore this UNESCO World Heritage Site.", + "locationName": "Uxmal", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "yucatan-peninsula", + "ref": "explore-the-ruins-of-uxmal", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/yucatan-peninsula_explore-the-ruins-of-uxmal.jpg" + }, + { + "name": "Stone Town Exploration", + "description": "Embark on a captivating journey through the labyrinthine streets of Stone Town, a UNESCO World Heritage site. Discover its rich history, architectural marvels like the House of Wonders and the Old Fort, and vibrant cultural tapestry. Get lost in the bustling bazaars, savor aromatic spices, and immerse yourself in the unique blend of African, Arab, and European influences.", + "locationName": "Stone Town", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "zanzibar", + "ref": "stone-town-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_stone-town-exploration.jpg" + }, + { + "name": "Spice Tour Adventure", + "description": "Unleash your inner spice enthusiast with a fragrant spice tour. Explore lush spice plantations, learn about the cultivation of cloves, nutmeg, cinnamon, and other exotic spices, and indulge in the tantalizing aromas and flavors. Discover the medicinal and culinary uses of these spices, and experience the island's rich agricultural heritage.", + "locationName": "Spice Plantations", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "zanzibar", + "ref": "spice-tour-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_spice-tour-adventure.jpg" + }, + { + "name": "Underwater Wonders: Scuba Diving or Snorkeling", + "description": "Dive into the vibrant underwater world surrounding Zanzibar. Explore coral reefs teeming with marine life, encounter colorful fish, graceful sea turtles, and perhaps even dolphins. Whether you choose scuba diving or snorkeling, prepare to be amazed by the beauty and biodiversity of the Indian Ocean.", + "locationName": "Mnemba Atoll or other diving/snorkeling sites", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": false, + "price": 3, + "destinationRef": "zanzibar", + "ref": "underwater-wonders-scuba-diving-or-snorkeling", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_underwater-wonders-scuba-diving-or-snorkeling.jpg" + }, + { + "name": "Prison Island Escape", + "description": "Take a short boat trip to Prison Island, also known as Changuu Island. Discover the island's intriguing history as a former quarantine station and prison, and encounter the resident giant tortoises. Relax on pristine beaches, swim in crystal-clear waters, and enjoy a peaceful escape from the mainland.", + "locationName": "Prison Island", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "zanzibar", + "ref": "prison-island-escape", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_prison-island-escape.jpg" + }, + { + "name": "Sunset Dhow Cruise", + "description": "Sail into the golden sunset aboard a traditional dhow, a wooden sailboat. As you glide along the tranquil waters, soak up the breathtaking views of the coastline, enjoy the gentle sea breeze, and witness the magical colors of the sky. Indulge in a delicious seafood dinner on board and create unforgettable memories.", + "locationName": "Zanzibar Coast", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 3, + "destinationRef": "zanzibar", + "ref": "sunset-dhow-cruise", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_sunset-dhow-cruise.jpg" + }, + { + "name": "Jozani Forest Exploration", + "description": "Embark on a journey into the heart of Zanzibar's Jozani Forest, a haven of biodiversity and home to the rare and endemic red colobus monkey. Hike through the lush mangrove swamps and witness these playful primates swinging through the trees. Keep an eye out for other fascinating creatures like bushbabies, duikers, and various bird species. This is a perfect adventure for nature enthusiasts and wildlife watchers.", + "locationName": "Jozani Chwaka Bay National Park", + "duration": 3, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "zanzibar", + "ref": "jozani-forest-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_jozani-forest-exploration.jpg" + }, + { + "name": "Kitesurfing in Paje", + "description": "Experience the thrill of kitesurfing in Paje, a renowned destination for watersports enthusiasts. With consistent winds, shallow turquoise waters, and a vibrant kitesurfing community, Paje offers ideal conditions for both beginners and experienced riders. Take lessons, rent equipment, and soar across the Indian Ocean, enjoying the adrenaline rush and breathtaking coastal views.", + "locationName": "Paje Beach", + "duration": 4, + "timeOfDay": "afternoon", + "familyFriendly": false, + "price": 3, + "destinationRef": "zanzibar", + "ref": "kitesurfing-in-paje", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_kitesurfing-in-paje.jpg" + }, + { + "name": "Forodhani Gardens Food Market", + "description": "Immerse yourself in the vibrant atmosphere of Forodhani Gardens, a bustling food market that comes alive each evening. Sample a diverse array of local Zanzibari street food, from fresh seafood and grilled meats to aromatic curries and tropical fruits. Enjoy the lively ambiance, mingle with locals, and savor the authentic flavors of Zanzibar under the starry sky.", + "locationName": "Forodhani Gardens", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "zanzibar", + "ref": "forodhani-gardens-food-market", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_forodhani-gardens-food-market.jpg" + }, + { + "name": "Spice Farm Tour and Cooking Class", + "description": "Delve into the world of spices on a captivating tour of a local spice farm. Discover the history and cultivation of Zanzibar's renowned spices, such as cloves, nutmeg, and cinnamon. Engage your senses with the vibrant colors and aromas, and then participate in a hands-on cooking class, learning to prepare traditional Zanzibari dishes infused with these aromatic spices. ", + "locationName": "Zanzibar Spice Farms", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 2, + "destinationRef": "zanzibar", + "ref": "spice-farm-tour-and-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_spice-farm-tour-and-cooking-class.jpg" + }, + { + "name": "Dhow Sailing and Island Hopping", + "description": "Embark on a traditional dhow sailing adventure, exploring the surrounding islands and hidden coves. Sail across the turquoise waters, snorkel in pristine coral reefs, and discover secluded beaches. Visit nearby islands like Mnemba Atoll or Chumbe Island, known for their abundant marine life and untouched natural beauty. Enjoy a leisurely day of sailing, swimming, and soaking up the sun.", + "locationName": "Zanzibar Archipelago", + "duration": 6, + "timeOfDay": "any", + "familyFriendly": true, + "price": 3, + "destinationRef": "zanzibar", + "ref": "dhow-sailing-and-island-hopping", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_dhow-sailing-and-island-hopping.jpg" + }, + { + "name": "Safari Blue Adventure", + "description": "Embark on a full-day sailing excursion to explore the Menai Bay Conservation Area. Swim with dolphins, snorkel amidst vibrant coral reefs, indulge in a seafood feast on a sandbank, and experience the magic of Zanzibar's turquoise waters.", + "locationName": "Menai Bay Conservation Area", + "duration": 8, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 4, + "destinationRef": "zanzibar", + "ref": "safari-blue-adventure", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_safari-blue-adventure.jpg" + }, + { + "name": "The Rock Restaurant Experience", + "description": "Dine at the iconic The Rock Restaurant, perched on a rock formation in the Indian Ocean. Savor delicious seafood and local cuisine while enjoying breathtaking panoramic views of the ocean.", + "locationName": "Michamvi Pingwe", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 4, + "destinationRef": "zanzibar", + "ref": "the-rock-restaurant-experience", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_the-rock-restaurant-experience.jpg" + }, + { + "name": "Mtoni Palace Ruins Exploration", + "description": "Step back in time and explore the haunting ruins of Mtoni Palace, once a majestic residence of Zanzibar's sultans. Discover the palace's history and architectural beauty, and imagine the opulent lifestyle of its former inhabitants.", + "locationName": "Mtoni", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "zanzibar", + "ref": "mtoni-palace-ruins-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_mtoni-palace-ruins-exploration.jpg" + }, + { + "name": "Kendwa Rocks Beach Party", + "description": "Experience Zanzibar's vibrant nightlife at Kendwa Rocks, a renowned beach bar and party hotspot. Dance the night away to live music, DJs, and enjoy the energetic atmosphere under the stars.", + "locationName": "Kendwa", + "duration": 5, + "timeOfDay": "night", + "familyFriendly": false, + "price": 3, + "destinationRef": "zanzibar", + "ref": "kendwa-rocks-beach-party", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_kendwa-rocks-beach-party.jpg" + }, + { + "name": "Zanzibar Butterfly Centre", + "description": "Immerse yourself in the enchanting world of butterflies at the Zanzibar Butterfly Centre. Witness a kaleidoscope of colors as you walk through the enclosed tropical garden and learn about the life cycle of these fascinating creatures.", + "locationName": "Pete", + "duration": 2, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "zanzibar", + "ref": "zanzibar-butterfly-centre", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_zanzibar-butterfly-centre.jpg" + }, + { + "name": "Dolphin Encounter in Kizimkazi", + "description": "Embark on a magical boat trip to Kizimkazi, a fishing village known for its resident population of dolphins. Watch these playful creatures in their natural habitat, and if you're lucky, you might even get the chance to swim alongside them. This experience is perfect for nature lovers and families with children.", + "locationName": "Kizimkazi", + "duration": 4, + "timeOfDay": "morning", + "familyFriendly": true, + "price": 3, + "destinationRef": "zanzibar", + "ref": "dolphin-encounter-in-kizimkazi", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_dolphin-encounter-in-kizimkazi.jpg" + }, + { + "name": "Mangrove Kayaking in Mtoni Marine Reserve", + "description": "Explore the serene beauty of the Mtoni Marine Reserve on a kayak adventure through the mangrove forests. Paddle through the calm waters, observe the diverse ecosystem, and learn about the importance of mangrove conservation. This activity is suitable for all ages and fitness levels, offering a unique perspective of Zanzibar's natural landscape.", + "locationName": "Mtoni Marine Reserve", + "duration": 3, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "zanzibar", + "ref": "mangrove-kayaking-in-mtoni-marine-reserve", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_mangrove-kayaking-in-mtoni-marine-reserve.jpg" + }, + { + "name": "Sunset at Nungwi Beach", + "description": "Experience the breathtaking beauty of a Zanzibar sunset at Nungwi Beach. Relax on the soft sand, sip on a refreshing cocktail, and watch as the sky transforms into a canvas of vibrant colors. Nungwi Beach offers a lively atmosphere with beach bars and restaurants, making it a perfect spot for a romantic evening or a fun night out with friends.", + "locationName": "Nungwi Beach", + "duration": 2, + "timeOfDay": "evening", + "familyFriendly": true, + "price": 1, + "destinationRef": "zanzibar", + "ref": "sunset-at-nungwi-beach", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_sunset-at-nungwi-beach.jpg" + }, + { + "name": "Authentic Swahili Cooking Class", + "description": "Immerse yourself in the local culture with a hands-on Swahili cooking class. Learn the secrets of traditional Zanzibari cuisine, using fresh, local ingredients and aromatic spices. Prepare and savor delicious dishes like pilau rice, coconut bean stew, and grilled seafood. This is a fantastic way to experience the culinary heritage of Zanzibar.", + "locationName": "Various locations", + "duration": 3, + "timeOfDay": "afternoon", + "familyFriendly": true, + "price": 3, + "destinationRef": "zanzibar", + "ref": "authentic-swahili-cooking-class", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_authentic-swahili-cooking-class.jpg" + }, + { + "name": "Jambiani Village and Kuza Cave Exploration", + "description": "Discover the authentic village life of Jambiani, known for its seaweed farming and traditional fishing practices. Interact with the friendly locals, learn about their culture, and explore the Kuza Cave, a natural wonder with crystal-clear water perfect for a refreshing swim. This off-the-beaten-path experience offers a glimpse into the heart of Zanzibar.", + "locationName": "Jambiani Village", + "duration": 4, + "timeOfDay": "any", + "familyFriendly": true, + "price": 2, + "destinationRef": "zanzibar", + "ref": "jambiani-village-and-kuza-cave-exploration", + "imageUrl": "https://storage.googleapis.com/tripedia-images/activities/zanzibar_jambiani-village-and-kuza-cave-exploration.jpg" + } +] diff --git a/compass_25/assets/destinations.json b/compass_25/assets/destinations.json new file mode 100644 index 0000000..0c7a8e3 --- /dev/null +++ b/compass_25/assets/destinations.json @@ -0,0 +1,1783 @@ +[ + { + "ref": "alaska", + "name": "Alaska", + "country": "United States", + "continent": "North America", + "knownFor": "Alaska is a haven for outdoor enthusiasts and nature lovers. Visitors can experience glaciers, mountains, and wildlife, making it ideal for hiking, kayaking, and wildlife viewing. Alaska also offers a unique cultural experience with its rich Native American heritage and frontier spirit.", + "tags": ["Mountain", "Off-the-beaten-path", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/alaska.jpg", + "location": { + "latitude": 64.2008, + "longitude": -149.4937 + } + }, + { + "ref": "amalfi-coast", + "name": "Amalfi Coast", + "country": "Italy", + "continent": "Europe", + "knownFor": "Experience the breathtaking beauty of the Amalfi Coast, with its dramatic cliffs, colorful villages, and turquoise waters. Indulge in delicious Italian cuisine, explore charming towns like Positano and Amalfi, and soak up the sun on picturesque beaches.", + "tags": ["Beach", "Romantic", "Foodie"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/amalfi-coast.jpg", + "location": { + "latitude": 40.6333, + "longitude": 14.6027 + } + }, + { + "ref": "amazon-rainforest", + "name": "Amazon Rainforest", + "country": "Brazil", + "continent": "South America", + "knownFor": "Immerse yourself in the biodiversity of the world's largest rainforest. Embark on jungle treks, spot exotic wildlife, and discover indigenous cultures. Take a boat trip down the Amazon River, explore the canopy on a zipline, and experience the unique ecosystem.", + "tags": ["Jungle", "Wildlife watching", "Adventure sports"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/amazon-rainforest.jpg", + "location": { + "latitude": -3.4653, + "longitude": -62.2159 + } + }, + { + "ref": "andes-mountains", + "name": "The Andes Mountains", + "country": "South America", + "continent": "South America", + "knownFor": "The Andes Mountains, stretching along the western coast of South America, offer diverse landscapes and experiences. Visitors can trek to Machu Picchu in Peru, explore the salt flats of Salar de Uyuni in Bolivia, or visit the glaciers of Patagonia. The Andes also provide opportunities for skiing, mountaineering, and cultural encounters with indigenous communities.", + "tags": ["Mountain", "Hiking", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/andes-mountains.jpg", + "location": { + "latitude": -13.1631, + "longitude": -72.5450 + } + }, + { + "ref": "angkor-wat", + "name": "Angkor Wat", + "country": "Cambodia", + "continent": "Asia", + "knownFor": "Angkor Wat, a vast temple complex in Cambodia, is the largest religious monument in the world and a UNESCO World Heritage site. Visitors can explore the intricate carvings, towering spires, and vast courtyards, marveling at the architectural grandeur and rich history of the Khmer Empire.", + "tags": ["Historic", "Cultural experiences", "Sightseeing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/angkor-wat.jpg", + "location": { + "latitude": 13.4125, + "longitude": 103.8660 + } + }, + { + "ref": "antelope-canyon", + "name": "Antelope Canyon", + "country": "United States", + "continent": "North America", + "knownFor": "Experience the awe-inspiring beauty of Antelope Canyon, a slot canyon renowned for its flowing sandstone formations and mesmerizing light beams. Embark on guided tours to navigate the narrow passageways and capture stunning photographs. Learn about the Navajo Nation's history and culture, as the canyon is located on their land.", + "tags": ["Off-the-beaten-path", "Hiking", "Sightseeing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/antelope-canyon.jpg", + "location": { + "latitude": 36.8619, + "longitude": -111.3743 + } + }, + { + "ref": "aruba", + "name": "Aruba", + "country": "Aruba", + "continent": "South America", + "knownFor": "Indulge in the beauty of Aruba, a Caribbean paradise known for its pristine beaches, turquoise waters, and year-round sunshine. Relax on the white sands of Eagle Beach, explore the vibrant capital of Oranjestad, and discover hidden coves and natural wonders. Aruba offers a perfect escape for beach lovers and water sports enthusiasts.", + "tags": ["Beach", "Island", "Scuba diving"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/aruba.jpg", + "location": { + "latitude": 12.5211, + "longitude": -69.9683 + } + }, + { + "ref": "asheville", + "name": "Asheville", + "country": "USA", + "continent": "North America", + "knownFor": "Asheville, nestled in the Blue Ridge Mountains of North Carolina, is a vibrant city known for its arts scene, craft breweries, and outdoor activities. Visitors can explore the historic Biltmore Estate, hike in the surrounding mountains, sample local beers, and enjoy the city's eclectic shops and restaurants.", + "tags": ["City", "Hiking", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/asheville.jpg", + "location": { + "latitude": 35.5951, + "longitude": -82.5515 + } + }, + { + "ref": "azores", + "name": "Azores", + "country": "Portugal", + "continent": "Europe", + "knownFor": "This archipelago in the mid-Atlantic boasts stunning volcanic landscapes, lush green hills, and dramatic coastlines. Hike to crater lakes, soak in natural hot springs, or go whale watching in the surrounding waters. With its relaxed atmosphere and unique culture, the Azores is a perfect destination for nature lovers and adventurers seeking an off-the-beaten-path experience.", + "tags": ["Island", "Off-the-beaten-path", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/azores.jpg", + "location": { + "latitude": 37.7412, + "longitude": -25.6756 + } + }, + { + "ref": "bali", + "name": "Bali", + "country": "Indonesia", + "continent": "Asia", + "knownFor": "Discover the cultural heart of Indonesia on the island of Bali. Explore ancient temples, experience vibrant Hindu traditions, and relax on beautiful beaches. Practice yoga and meditation, indulge in spa treatments, and enjoy the island's lush natural beauty.", + "tags": ["Island", "Cultural experiences", "Wellness retreats"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/bali.jpg", + "location": { + "latitude": -8.3405, + "longitude": 115.0917 + } + }, + { + "ref": "banff-national-park", + "name": "Banff National Park", + "country": "Canada", + "continent": "North America", + "knownFor": "Nestled in the Canadian Rockies, Banff National Park offers stunning mountain scenery, turquoise lakes, and abundant wildlife. Hike to Lake Louise, explore Johnston Canyon, and enjoy outdoor activities year-round.", + "tags": ["Mountain", "Hiking", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/banff-national-park.jpg", + "location": { + "latitude": 51.4968, + "longitude": -115.9281 + } + }, + { + "ref": "belize", + "name": "Belize", + "country": "Belize", + "continent": "North America", + "knownFor": "Embark on an unforgettable adventure in Belize, a Central American gem boasting lush rainforests, ancient Maya ruins, and the world's second-largest barrier reef. Explore the mysteries of Caracol and Xunantunich, dive into the Great Blue Hole, and discover diverse marine life. Belize offers a unique blend of cultural exploration and eco-tourism.", + "tags": ["Jungle", "Historic", "Scuba diving"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/belize.jpg", + "location": { + "latitude": 17.1899, + "longitude": -88.4976 + } + }, + { + "ref": "bhutan", + "name": "Bhutan", + "country": "Bhutan", + "continent": "Asia", + "knownFor": "Discover the mystical kingdom of Bhutan, nestled in the Himalayas. Explore ancient monasteries, hike through pristine valleys, and experience the unique culture and traditions. Immerse yourself in the spiritual atmosphere and embrace the concept of Gross National Happiness.", + "tags": ["Mountain", "Cultural experiences", "Off-the-beaten-path"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/bhutan.jpg", + "location": { + "latitude": 27.5142, + "longitude": 90.4336 + } + }, + { + "ref": "big-island-hawaii", + "name": "Big Island", + "country": "United States", + "continent": "North America", + "knownFor": "The Big Island of Hawaii offers diverse landscapes, from volcanic craters and black sand beaches to lush rainforests and snow-capped mountains. Visitors can witness the fiery glow of Kilauea volcano, snorkel with manta rays, and stargaze atop Mauna Kea.", + "tags": ["Island", "Hiking", "Snorkeling"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/big-island-hawaii.jpg", + "location": { + "latitude": 19.5429, + "longitude": -155.6659 + } + }, + { + "ref": "big-sur", + "name": "Big Sur", + "country": "United States", + "continent": "North America", + "knownFor": "Experience the breathtaking beauty of California's rugged coastline along Big Sur. Drive the iconic Pacific Coast Highway, stopping at dramatic cliffs, hidden coves, and redwood forests. Enjoy hiking, camping, or simply soaking up the awe-inspiring views of this natural wonderland.", + "tags": ["Road trip destination", "Secluded", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/big-sur.jpg", + "location": { + "latitude": 36.2704, + "longitude": -121.8081 + } + }, + { + "ref": "bora-bora", + "name": "Bora Bora", + "country": "French Polynesia", + "continent": "Oceania", + "knownFor": "Bora Bora is synonymous with luxury and tranquility. Overwater bungalows perched above turquoise lagoons offer a unique and indulgent experience. Visitors can enjoy snorkeling, diving, and swimming amidst vibrant coral reefs and diverse marine life. The island's lush interior provides opportunities for hiking and exploring, while Polynesian culture adds a touch of exotic charm.", + "tags": ["Island", "Luxury", "Secluded"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/bora-bora.jpg", + "location": { + "latitude": -16.5004, + "longitude": -151.7415 + } + }, + { + "ref": "botswana", + "name": "Okavango Delta", + "country": "Botswana", + "continent": "Africa", + "knownFor": "The Okavango Delta, a unique inland delta in Botswana, is a haven for wildlife enthusiasts. Visitors can embark on safari adventures, encountering elephants, lions, hippos, and a diverse array of birds. The delta's waterways offer opportunities for mokoro (canoe) excursions and boat tours, providing a close-up view of the abundant wildlife and stunning landscapes.", + "tags": ["Wildlife watching", "Adventure sports", "Luxury"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/botswana.jpg", + "location": { + "latitude": -19.4318, + "longitude": 22.8112 + } + }, + { + "ref": "bruges", + "name": "Bruges", + "country": "Belgium", + "continent": "Europe", + "knownFor": "Step back in time in this charming medieval city with its cobblestone streets, canals, and well-preserved architecture. Explore the historic Markt square, indulge in delicious Belgian chocolate and beer, or take a boat tour through the canals. Bruges offers a romantic and picturesque escape for history buffs and those seeking a quintessential European experience.", + "tags": ["City", "Historic", "Romantic"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/bruges.jpg", + "location": { + "latitude": 51.2093, + "longitude": 3.2247 + } + }, + { + "ref": "brunei", + "name": "Brunei", + "country": "Brunei", + "continent": "Asia", + "knownFor": "This small sultanate on the island of Borneo offers a fascinating blend of culture and nature. Visitors can explore the opulent Sultan Omar Ali Saifuddin Mosque, delve into the lush rainforests, and discover the unique water village of Kampong Ayer.", + "tags": ["Secluded", "Cultural experiences", "Island"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/brunei.jpg", + "location": { + "latitude": 4.5353, + "longitude": 114.7277 + } + }, + { + "ref": "budapest", + "name": "Budapest", + "country": "Hungary", + "continent": "Europe", + "knownFor": "Discover the charm of Budapest, a historic city divided by the Danube River. Explore Buda's Castle District with its medieval streets and Fisherman's Bastion, offering panoramic views. Relax in the thermal baths, a legacy of the Ottoman era, or visit the Hungarian Parliament Building, a stunning example of Gothic Revival architecture.", + "tags": ["City", "Historic", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/budapest.jpg", + "location": { + "latitude": 47.4979, + "longitude": 19.0402 + } + }, + { + "ref": "burgundy", + "name": "Burgundy", + "country": "France", + "continent": "Europe", + "knownFor": "Burgundy, a region in eastern France, is renowned for its world-class wines, charming villages, and rich history. Explore vineyards, indulge in wine tastings, and visit medieval castles and abbeys. Cycle through rolling hills, savor gourmet cuisine, and experience the art de vivre of this picturesque region.", + "tags": ["Rural", "Wine tasting", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/burgundy.jpg", + "location": { + "latitude": 47.1391, + "longitude": 4.5848 + } + }, + { + "ref": "cambodia", + "name": "Cambodia", + "country": "Cambodia", + "continent": "Asia", + "knownFor": "Cambodia, a Southeast Asian nation, is a captivating blend of ancient wonders, natural beauty, and vibrant culture. Explore the magnificent temples of Angkor, relax on pristine beaches, and cruise along the Mekong River. Experience the warmth of the Cambodian people, savor delicious cuisine, and discover the rich history of this fascinating country.", + "tags": ["Historic", "Cultural experiences", "Beach"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/cambodia.jpg", + "location": { + "latitude": 12.5657, + "longitude": 104.9910 + } + }, + { + "ref": "canadian-rockies", + "name": "Canadian Rockies", + "country": "Canada", + "continent": "North America", + "knownFor": "Embark on an adventure through the majestic Canadian Rockies, where towering mountains, turquoise lakes, and glaciers create a breathtaking landscape. Hike through scenic trails, go skiing or snowboarding in world-class resorts, and encounter diverse wildlife. Experience the thrill of outdoor activities and the beauty of untouched nature.", + "tags": ["Mountain", "Hiking", "Adventure sports"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/canadian-rockies.jpg", + "location": { + "latitude": 52.1330, + "longitude": -116.9174 + } + }, + { + "ref": "canary-islands", + "name": "Canary Islands", + "country": "Spain", + "continent": "Europe", + "knownFor": "Discover the volcanic beauty of the Canary Islands, a Spanish archipelago off the coast of Africa. Explore diverse landscapes, from the dramatic volcanic peaks of Tenerife and Mount Teide to the golden beaches of Gran Canaria and Fuerteventura. Enjoy water sports, hiking, and stargazing in this island paradise.", + "tags": ["Island", "Beach", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/canary-islands.jpg", + "location": { + "latitude": 28.2916, + "longitude": -16.6291 + } + }, + { + "ref": "cappadocia", + "name": "Cappadocia", + "country": "Turkey", + "continent": "Asia", + "knownFor": "Embark on a magical journey through a surreal landscape of fairy chimneys, cave dwellings, and underground cities. Soar above the valleys in a hot air balloon, explore ancient rock-cut churches, and experience the unique culture and hospitality of the region.", + "tags": ["Historic", "Off-the-beaten-path", "Adventure sports"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/cappadocia.jpg", + "location": { + "latitude": 38.6431, + "longitude": 34.8280 + } + }, + { + "ref": "chilean-lake-district", + "name": "Chilean Lake District", + "country": "Chile", + "continent": "South America", + "knownFor": "The Chilean Lake District is a paradise for nature lovers, with snow-capped volcanoes, turquoise lakes, and lush forests. Hike in national parks, go kayaking on the lakes, and enjoy the tranquility of the surroundings. The region's German heritage adds a unique cultural element.", + "tags": ["Lake", "Mountain", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/chilean-lake-district.jpg", + "location": { + "latitude": -39.8142, + "longitude": -73.0459 + } + }, + { + "ref": "cinque-terre", + "name": "Cinque Terre", + "country": "Italy", + "continent": "Europe", + "knownFor": "Explore the picturesque villages of Cinque Terre, perched on the rugged Italian Riviera coastline. Hike the scenic trails connecting the five colorful towns, each with its unique character. Enjoy fresh seafood, local wines, and breathtaking views of the Mediterranean Sea.", + "tags": ["Hiking", "Coastal", "Foodie"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/cinque-terre.jpg", + "location": { + "latitude": 44.1284, + "longitude": 9.7094 + } + }, + { + "ref": "colombia", + "name": "Colombia", + "country": "Colombia", + "continent": "South America", + "knownFor": "Colombia is a vibrant country with a diverse landscape, ranging from the Andes Mountains to the Caribbean coast. Explore the colonial city of Cartagena, hike in the Cocora Valley, or dance the night away in Medellín. Discover the coffee region, learn about the indigenous cultures, and experience the warmth of the Colombian people.", + "tags": ["City", "Mountain", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/colombia.jpg", + "location": { + "latitude": 4.5709, + "longitude": -74.2973 + } + }, + { + "ref": "corsica", + "name": "Corsica", + "country": "France", + "continent": "Europe", + "knownFor": "This mountainous Mediterranean island offers a diverse landscape of rugged mountains, pristine beaches, and charming villages. Visitors can enjoy hiking, water sports, exploring historical sites, and experiencing the unique Corsican culture.", + "tags": ["Mountain", "Beach", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/corsica.jpg", + "location": { + "latitude": 42.0396, + "longitude": 9.0129 + } + }, + { + "ref": "costa-rica", + "name": "Osa Peninsula", + "country": "Costa Rica", + "continent": "North America", + "knownFor": "A haven for eco-tourism, the Osa Peninsula boasts incredible biodiversity, lush rainforests, and pristine beaches. Adventure seekers can go zip-lining, kayaking, and hiking, while nature enthusiasts can spot monkeys, sloths, and exotic birds. The Corcovado National Park, known as one of the most biodiverse places on Earth, is a must-visit for wildlife watching.", + "tags": ["Jungle", "Secluded", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/costa-rica.jpg", + "location": { + "latitude": 8.5380, + "longitude": -83.5910 + } + }, + { + "ref": "dubai", + "name": "Dubai", + "country": "United Arab Emirates", + "continent": "Asia", + "knownFor": "Dubai is a modern metropolis known for its towering skyscrapers, luxurious shopping malls, and extravagant attractions. Visitors can experience the thrill of the Burj Khalifa, the world's tallest building, shop at the Dubai Mall, or enjoy a desert safari. With its vibrant nightlife and world-class dining scene, Dubai offers a truly cosmopolitan experience.", + "tags": ["City", "Luxury", "Shopping"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/dubai.jpg", + "location": { + "latitude": 25.2048, + "longitude": 55.2708 + } + }, + { + "ref": "dubrovnik", + "name": "Dubrovnik", + "country": "Croatia", + "continent": "Europe", + "knownFor": "Dubrovnik, the \"Pearl of the Adriatic\", is a historic walled city renowned for its stunning architecture, ancient city walls, and breathtaking coastal views. Visitors can explore historical sites, enjoy boat trips, and experience the vibrant cultural scene.", + "tags": ["Historic", "Sightseeing", "City"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/dubrovnik.jpg", + "location": { + "latitude": 42.6507, + "longitude": 18.0944 + } + }, + { + "ref": "ethiopia", + "name": "Ethiopia", + "country": "Ethiopia", + "continent": "Africa", + "knownFor": "Ethiopia, a landlocked country in the Horn of Africa, is a unique destination with ancient history, diverse landscapes, and rich culture. Explore the rock-hewn churches of Lalibela, trek through the Simien Mountains, and witness the vibrant tribal traditions. From bustling cities to remote villages, Ethiopia offers an unforgettable journey.", + "tags": ["Off-the-beaten-path", "Cultural experiences", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/ethiopia.jpg", + "location": { + "latitude": 9.1450, + "longitude": 40.4897 + } + }, + { + "ref": "fiesole", + "name": "Fiesole", + "country": "Italy", + "continent": "Europe", + "knownFor": "Step back in time in Fiesole, a charming hilltop town overlooking Florence, Italy. Explore Etruscan and Roman ruins, visit the beautiful Fiesole Cathedral, and wander through quaint streets lined with historic buildings. Enjoy breathtaking views of the Tuscan countryside and escape the hustle and bustle of Florence.", + "tags": ["Historic", "Rural", "Romantic"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/fiesole.jpg", + "location": { + "latitude": 43.8058, + "longitude": 11.2931 + } + }, + { + "ref": "fiji", + "name": "Fiji", + "country": "Fiji", + "continent": "Oceania", + "knownFor": "Fiji, an archipelago in the South Pacific, is renowned for its pristine beaches, crystal-clear waters, and lush rainforests. Visitors can indulge in water activities like snorkeling, scuba diving, and surfing, or explore the islands' rich cultural heritage and traditions. Fiji is also a popular destination for honeymoons and romantic getaways.", + "tags": ["Island", "Beach", "Scuba diving"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/fiji.jpg", + "location": { + "latitude": -17.7134, + "longitude": 178.0650 + } + }, + { + "ref": "french-alps", + "name": "French Alps", + "country": "France", + "continent": "Europe", + "knownFor": "The French Alps offer stunning mountain scenery, charming villages, and world-class skiing. During the winter months, hit the slopes in renowned resorts like Chamonix and Val d'Isère. In the summer, enjoy hiking, mountain biking, and paragliding.", + "tags": ["Mountain", "Skiing", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/french-alps.jpg", + "location": { + "latitude": 45.8326, + "longitude": 6.8651 + } + }, + { + "ref": "french-polynesia", + "name": "French Polynesia", + "country": "France", + "continent": "Oceania", + "knownFor": "Escape to the paradise of French Polynesia, where overwater bungalows, turquoise lagoons, and pristine beaches await. Dive into the vibrant underwater world, explore volcanic landscapes, and experience Polynesian culture. Indulge in luxury resorts, romantic getaways, and unforgettable island hopping adventures.", + "tags": ["Tropical", "Island", "Luxury"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/french-polynesia.jpg", + "location": { + "latitude": -17.6797, + "longitude": -149.4068 + } + }, + { + "ref": "french-riviera", + "name": "French Riviera", + "country": "France", + "continent": "Europe", + "knownFor": "Discover the glamour of the French Riviera, where glamorous beaches, luxury yachts, and charming coastal towns line the Mediterranean coast. Explore the vibrant city of Nice, visit the iconic Monte Carlo casino, and soak up the sun on the beaches of Cannes. Experience the region's luxurious lifestyle, stunning scenery, and vibrant nightlife.", + "tags": ["Beach", "Luxury", "Nightlife"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/french-riviera.jpg", + "location": { + "latitude": 43.7031, + "longitude": 7.2661 + } + }, + { + "ref": "galapagos-islands", + "name": "Galapagos Islands", + "country": "Ecuador", + "continent": "South America", + "knownFor": "Embark on a once-in-a-lifetime adventure to the Galapagos Islands, a living laboratory of evolution. Observe unique wildlife, including giant tortoises, marine iguanas, and blue-footed boobies. Go snorkeling or diving among vibrant coral reefs and explore volcanic landscapes.", + "tags": ["Island", "Wildlife watching", "Scuba diving"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/galapagos-islands.jpg", + "location": { + "latitude": -0.7399, + "longitude": -90.3126 + } + }, + { + "ref": "galicia", + "name": "Galicia", + "country": "Spain", + "continent": "Europe", + "knownFor": "This region in northwestern Spain boasts rugged coastlines, green landscapes, and a rich Celtic heritage. Visitors can enjoy fresh seafood, explore charming towns, embark on the Camino de Santiago pilgrimage, and discover the unique Galician culture.", + "tags": ["Hiking", "Cultural experiences", "Foodie"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/galicia.jpg", + "location": { + "latitude": 42.8805, + "longitude": -8.5457 + } + }, + { + "ref": "grand-canyon", + "name": "Grand Canyon", + "country": "United States", + "continent": "North America", + "knownFor": "Witness the awe-inspiring vastness and natural beauty of the Grand Canyon, a UNESCO World Heritage Site. Hike along the rim for breathtaking panoramic views, descend into the canyon for a challenging adventure, or raft down the Colorado River. The Grand Canyon offers an unforgettable experience for nature enthusiasts and adventure seekers.", + "tags": ["Mountain", "Hiking", "Adventure sports"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/grand-canyon.jpg", + "location": { + "latitude": 36.1069, + "longitude": -112.1129 + } + }, + { + "ref": "great-barrier-reef", + "name": "Great Barrier Reef", + "country": "Australia", + "continent": "Australia", + "knownFor": "The Great Barrier Reef is the world's largest coral reef system, teeming with marine life. Snorkel or scuba dive among colorful corals, spot tropical fish, and experience the underwater wonders of this natural treasure.", + "tags": ["Snorkeling", "Scuba diving", "Adventure sports"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/great-barrier-reef.jpg", + "location": { + "latitude": -18.2871, + "longitude": 147.6992 + } + }, + { + "ref": "great-bear-rainforest", + "name": "Great Bear Rainforest", + "country": "Canada", + "continent": "North America", + "knownFor": "The Great Bear Rainforest is a vast and pristine wilderness area on the Pacific coast of British Columbia. It is home to a diverse array of wildlife, including grizzly bears, black bears, wolves, and whales. Visitors can explore the rainforest by boat, kayak, or on foot, and experience the magic of this untouched ecosystem.", + "tags": ["Wildlife watching", "Hiking", "Eco-conscious"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/great-bear-rainforest.jpg", + "location": { + "latitude": 52.3713, + "longitude": -126.7532 + } + }, + { + "ref": "greek-islands", + "name": "Greek Islands", + "country": "Greece", + "continent": "Europe", + "knownFor": "Island hop through the Greek Islands, each with its own unique charm and allure. Explore ancient ruins, relax on pristine beaches, and indulge in delicious Greek cuisine. Experience the vibrant nightlife of Mykonos, the romantic sunsets of Santorini, and the historical treasures of Crete. Discover a world of crystal-clear waters, whitewashed villages, and endless sunshine.", + "tags": ["Island", "Beach", "Historic"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/greek-islands.jpg", + "location": { + "latitude": 37.9908, + "longitude": 25.7033 + } + }, + { + "ref": "greenland", + "name": "Ilulissat Icefjord", + "country": "Greenland", + "continent": "North America", + "knownFor": "Ilulissat Icefjord is a UNESCO World Heritage site and a breathtaking natural wonder with massive icebergs calving from the Sermeq Kujalleq glacier. Visitors can take boat tours to witness the stunning ice formations, go hiking in the surrounding area, and experience the unique culture of Greenland.", + "tags": ["Off-the-beaten-path", "Adventure sports", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/greenland.jpg", + "location": { + "latitude": 69.1310, + "longitude": -49.9892 + } + }, + { + "ref": "guadeloupe", + "name": "Guadeloupe", + "country": "France", + "continent": "North America", + "knownFor": "Guadeloupe, a butterfly-shaped archipelago in the Caribbean, is a French overseas territory boasting stunning natural landscapes. With its lush rainforests, volcanic peaks, and white-sand beaches, it's a paradise for outdoor enthusiasts. Hike to cascading waterfalls, explore vibrant coral reefs, and savor the unique blend of French and Creole culture.", + "tags": ["Island", "Tropical", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/guadeloupe.jpg", + "location": { + "latitude": 16.2650, + "longitude": -61.5510 + } + }, + { + "ref": "guatemala", + "name": "Lake Atitlán", + "country": "Guatemala", + "continent": "North America", + "knownFor": "Lake Atitlán, surrounded by volcanoes and traditional Mayan villages, offers a scenic and cultural experience in Guatemala. Visitors can explore the lakeside towns, hike to viewpoints, visit Mayan ruins, and learn about local traditions. The lake also provides opportunities for kayaking, swimming, and boat trips.", + "tags": ["Lake", "Cultural experiences", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/guatemala.jpg", + "location": { + "latitude": 14.7000, + "longitude": -91.1833 + } + }, + { + "ref": "ha-long-bay", + "name": "Ha Long Bay", + "country": "Vietnam", + "continent": "Asia", + "knownFor": "Ha Long Bay is a breathtaking UNESCO World Heritage Site, featuring thousands of limestone islands and islets rising from emerald waters. Visitors can cruise through the bay, explore hidden caves, kayak among the karst formations, and experience the unique beauty of this natural wonder.", + "tags": ["Island", "Secluded", "Sightseeing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/ha-long-bay.jpg", + "location": { + "latitude": 20.9101, + "longitude": 107.1839 + } + }, + { + "ref": "harrisburg", + "name": "Harrisburg", + "country": "USA", + "continent": "North America", + "knownFor": "Discover the charm of Harrisburg, Pennsylvania's historic capital city. Explore the impressive Pennsylvania State Capitol Building, delve into history at the National Civil War Museum, and enjoy family fun at City Island. With its scenic riverfront location, Harrisburg offers a blend of cultural attractions and outdoor activities.", + "tags": ["City", "Historic", "Family-friendly"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/harrisburg.jpg", + "location": { + "latitude": 40.2732, + "longitude": -76.8867 + } + }, + { + "ref": "havana", + "name": "Havana", + "country": "Cuba", + "continent": "North America", + "knownFor": "Step back in time in Havana, the vibrant capital of Cuba. Stroll along the Malecón, a seaside promenade, and admire the colorful vintage cars. Explore Old Havana, a UNESCO World Heritage Site, with its colonial architecture and lively squares. Immerse yourself in Cuban culture, music, and dance.", + "tags": ["City", "Historic", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/havana.jpg", + "location": { + "latitude": 23.1136, + "longitude": -82.3666 + } + }, + { + "ref": "ho-chi-minh-city", + "name": "Ho Chi Minh City", + "country": "Vietnam", + "continent": "Asia", + "knownFor": "Ho Chi Minh City is a vibrant metropolis with a rich history and delicious street food. Explore the bustling markets, visit historical landmarks like the Cu Chi Tunnels, and immerse yourself in the city's energetic atmosphere. The blend of French colonial architecture and modern skyscrapers creates a unique cityscape.", + "tags": ["City", "Cultural experiences", "Food tours"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/ho-chi-minh-city.jpg", + "location": { + "latitude": 10.8231, + "longitude": 106.6297 + } + }, + { + "ref": "iceland", + "name": "Iceland", + "country": "Iceland", + "continent": "Europe", + "knownFor": "Iceland's dramatic landscapes include glaciers, volcanoes, geysers, and waterfalls. Explore the Golden Circle, relax in geothermal pools, and witness the Northern Lights in winter.", + "tags": ["Secluded", "Adventure sports", "Winter destination"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/iceland.jpg", + "location": { + "latitude": 64.9631, + "longitude": -19.0208 + } + }, + { + "ref": "japan-alps", + "name": "Japanese Alps", + "country": "Japan", + "continent": "Asia", + "knownFor": "The Japanese Alps offer stunning mountain scenery, traditional villages, and outdoor adventures. Hike through the Kamikochi Valley, ski in Hakuba, or soak in the onsen hot springs. Visit Matsumoto Castle, a historic landmark, and experience the unique culture of the mountain communities.", + "tags": ["Mountain", "Hiking", "Skiing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/japan-alps.jpg", + "location": { + "latitude": 36.2476, + "longitude": 137.6330 + } + }, + { + "ref": "jeju-island", + "name": "Jeju Island", + "country": "South Korea", + "continent": "Asia", + "knownFor": "Escape to the volcanic paradise of Jeju Island, a popular destination off the coast of South Korea. Discover stunning natural landscapes, from volcanic craters and lava tubes to pristine beaches and waterfalls. Explore unique museums, hike Mount Hallasan, and relax in charming coastal towns.", + "tags": ["Island", "Hiking", "Secluded"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/jeju-island.jpg", + "location": { + "latitude": 33.4890, + "longitude": 126.4983 + } + }, + { + "ref": "jordan", + "name": "Petra", + "country": "Jordan", + "continent": "Asia", + "knownFor": "Petra, an ancient city carved into rose-colored sandstone cliffs, is a UNESCO World Heritage site and one of the New Seven Wonders of the World. Visitors can marvel at the Treasury, explore the Siq, and discover hidden tombs and temples. Petra offers a glimpse into the fascinating history and culture of the Nabataean civilization.", + "tags": ["Historic", "Cultural experiences", "Off-the-beaten-path"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/jordan.jpg", + "location": { + "latitude": 30.3285, + "longitude": 35.4444 + } + }, + { + "ref": "kauai", + "name": "Kauai", + "country": "United States", + "continent": "North America", + "knownFor": "Escape to the Garden Isle of Kauai, a paradise of lush rainforests, dramatic cliffs, and pristine beaches. Hike the challenging Kalalau Trail along the Na Pali Coast, kayak the Wailua River, or relax on Poipu Beach. Discover hidden waterfalls, explore Waimea Canyon, and experience the island's laid-back atmosphere.", + "tags": ["Island", "Hiking", "Beach"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/kauai.jpg", + "location": { + "latitude": 22.0964, + "longitude": -159.5261 + } + }, + { + "ref": "kenya", + "name": "Kenya", + "country": "Kenya", + "continent": "Africa", + "knownFor": "Embark on an unforgettable safari adventure in Kenya, home to diverse wildlife and stunning landscapes. Witness the Great Migration, encounter lions, elephants, and rhinos, and experience the rich culture of the Maasai people.", + "tags": ["Wildlife watching", "Safari", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/kenya.jpg", + "location": { + "latitude": -0.0236, + "longitude": 37.9062 + } + }, + { + "ref": "kyoto", + "name": "Kyoto", + "country": "Japan", + "continent": "Asia", + "knownFor": "Kyoto, Japan's former capital, is a cultural treasure trove with numerous temples, shrines, and gardens. Experience traditional tea ceremonies, stroll through the Arashiyama Bamboo Grove, and immerse yourself in Japanese history and spirituality.", + "tags": ["Historic", "Cultural experiences", "City"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/kyoto.jpg", + "location": { + "latitude": 35.0116, + "longitude": 135.7681 + } + }, + { + "ref": "lake-bled", + "name": "Lake Bled", + "country": "Slovenia", + "continent": "Europe", + "knownFor": "Nestled in the Julian Alps, Lake Bled is a fairytale-like destination with a stunning glacial lake, a charming island church, and a medieval castle perched on a cliff. Visitors can enjoy swimming, boating, hiking, and exploring the surrounding mountains. Bled is also known for its delicious cream cake and thermal springs.", + "tags": ["Lake", "Mountain", "Romantic"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/lake-bled.jpg", + "location": { + "latitude": 46.3636, + "longitude": 14.0938 + } + }, + { + "ref": "lake-como", + "name": "Lake Como", + "country": "Italy", + "continent": "Europe", + "knownFor": "Escape to the picturesque shores of Lake Como, surrounded by charming villages, luxurious villas, and stunning mountain scenery. Take a boat tour on the lake, explore historic gardens, and indulge in fine dining and Italian wines. Enjoy hiking, biking, and water sports in the surrounding area.", + "tags": ["Lake", "Romantic", "Luxury"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/lake-como.jpg", + "location": { + "latitude": 45.9910, + "longitude": 9.2486 + } + }, + { + "ref": "lake-district", + "name": "Lake District National Park", + "country": "England", + "continent": "Europe", + "knownFor": "Nestled in the heart of Cumbria, the Lake District offers breathtaking landscapes with rolling hills, shimmering lakes, and charming villages. Visitors can enjoy hiking, boating, and exploring the literary legacy of Beatrix Potter and William Wordsworth. The region is also known for its delicious local produce and cozy pubs.", + "tags": ["Lake", "Hiking", "Rural"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/lake-district.jpg", + "location": { + "latitude": 54.4609, + "longitude": -3.0886 + } + }, + { + "ref": "lake-garda", + "name": "Lake Garda", + "country": "Italy", + "continent": "Europe", + "knownFor": "Lake Garda is the largest lake in Italy, known for its stunning scenery, charming towns, and historic sites. Visitors can enjoy swimming, sailing, windsurfing, and hiking in the surrounding mountains. The area is also famous for its lemon groves and olive oil production, offering delicious local cuisine and wine tasting experiences.", + "tags": ["Lake", "Mountain", "Food tours"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/lake-garda.jpg", + "location": { + "latitude": 45.6092, + "longitude": 10.6411 + } + }, + { + "ref": "lake-tahoe", + "name": "Lake Tahoe", + "country": "USA", + "continent": "North America", + "knownFor": "Lake Tahoe offers a blend of outdoor adventure and stunning natural beauty. Visitors enjoy skiing in the winter and water sports like kayaking and paddleboarding in the summer. The crystal-clear lake, surrounded by mountains, provides breathtaking scenery and a relaxing atmosphere.", + "tags": ["Lake", "Mountain", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/lake-tahoe.jpg", + "location": { + "latitude": 39.0968, + "longitude": -120.0324 + } + }, + { + "ref": "laos", + "name": "Laos", + "country": "Laos", + "continent": "Asia", + "knownFor": "Laos is a landlocked country in Southeast Asia, known for its laid-back atmosphere, stunning natural beauty, and ancient temples. Explore the UNESCO World Heritage city of Luang Prabang, kayak down the Mekong River, discover the Kuang Si Falls, and visit the Pak Ou Caves filled with thousands of Buddha statues.", + "tags": ["Off-the-beaten-path", "Cultural experiences", "River"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/laos.jpg", + "location": { + "latitude": 19.8563, + "longitude": 102.4955 + } + }, + { + "ref": "lofoten-islands", + "name": "Lofoten Islands", + "country": "Norway", + "continent": "Europe", + "knownFor": "Experience the breathtaking beauty of the Arctic Circle with dramatic landscapes, towering mountains, and charming fishing villages. Hike scenic trails, kayak along pristine fjords, and marvel at the Northern Lights. Explore Viking history and indulge in fresh seafood delicacies.", + "tags": ["Island", "Secluded", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/lofoten-islands.jpg", + "location": { + "latitude": 68.2167, + "longitude": 14.1667 + } + }, + { + "ref": "lombok", + "name": "Lombok", + "country": "Indonesia", + "continent": "Asia", + "knownFor": "Lombok, Bali's less-crowded neighbor, offers pristine beaches, lush rainforests, and the majestic Mount Rinjani volcano. Visitors can enjoy surfing, diving, hiking, and exploring the island's cultural attractions.", + "tags": ["Beach", "Mountain", "Adventure sports"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/lombok.jpg", + "location": { + "latitude": -8.6509, + "longitude": 116.3253 + } + }, + { + "ref": "luang-prabang", + "name": "Luang Prabang", + "country": "Laos", + "continent": "Asia", + "knownFor": "Immerse yourself in the tranquility of Luang Prabang, a UNESCO World Heritage town in Laos. Visit ornate temples like Wat Xieng Thong, witness the alms-giving ceremony at dawn, and explore the night market. Cruise down the Mekong River, discover Kuang Si Falls, and experience the town's spiritual atmosphere.", + "tags": ["Off-the-beaten-path", "Cultural experiences", "River"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/luang-prabang.jpg", + "location": { + "latitude": 19.8858, + "longitude": 102.1347 + } + }, + { + "ref": "madagascar", + "name": "Madagascar", + "country": "Madagascar", + "continent": "Africa", + "knownFor": "Madagascar, an island nation off the southeast coast of Africa, is a biodiversity hotspot with unique flora and fauna. Explore rainforests, baobab-lined avenues, and pristine beaches. Encounter lemurs, chameleons, and other endemic species, and discover the rich cultural heritage of the Malagasy people. Madagascar offers an unforgettable adventure for nature lovers.", + "tags": ["Island", "Wildlife watching", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/madagascar.jpg", + "location": { + "latitude": -18.7669, + "longitude": 46.8691 + } + }, + { + "ref": "maldives", + "name": "Maldives", + "country": "Maldives", + "continent": "Asia", + "knownFor": "The Maldives, a tropical island nation, offers luxurious overwater bungalows, pristine beaches, and world-class diving. Relax on the white sand, swim in the crystal-clear waters, and explore the vibrant coral reefs. The Maldives is the perfect destination for a romantic getaway or a relaxing beach vacation.", + "tags": ["Island", "Beach", "Scuba diving"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/maldives.jpg", + "location": { + "latitude": 3.2028, + "longitude": 73.2207 + } + }, + { + "ref": "mallorca", + "name": "Mallorca", + "country": "Spain", + "continent": "Europe", + "knownFor": "Mallorca offers a diverse experience, from stunning beaches and turquoise waters to charming villages and rugged mountains. Visitors can explore historic Palma, hike the Serra de Tramuntana, or simply relax on the beach and enjoy the Mediterranean sunshine.", + "tags": ["Beach", "Island", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/mallorca.jpg", + "location": { + "latitude": 39.6953, + "longitude": 3.0176 + } + }, + { + "ref": "malta", + "name": "Malta", + "country": "Malta", + "continent": "Europe", + "knownFor": "Malta, an archipelago in the central Mediterranean, is a captivating blend of history, culture, and stunning natural beauty. Its ancient temples, fortified cities, and hidden coves attract history buffs and adventurers alike. From exploring the UNESCO-listed capital Valletta to diving in crystal-clear waters, Malta offers a diverse experience for every traveler.", + "tags": ["Island", "Historic", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/malta.jpg", + "location": { + "latitude": 35.9375, + "longitude": 14.3754 + } + }, + { + "ref": "marrakech", + "name": "Marrakech", + "country": "Morocco", + "continent": "Africa", + "knownFor": "Step into a world of vibrant colours and bustling souks in Marrakech. Explore the historic Medina, with its maze-like alleys and hidden treasures, or marvel at the intricate architecture of mosques and palaces. Indulge in the rich flavours of Moroccan cuisine and experience the unique culture of this magical city.", + "tags": ["Cultural experiences", "City", "Shopping"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/marrakech.jpg", + "location": { + "latitude": 31.6295, + "longitude": -7.9811 + } + }, + { + "ref": "maui", + "name": "Maui", + "country": "United States", + "continent": "North America", + "knownFor": "Experience the diverse landscapes of Maui, from volcanic craters to lush rainforests and stunning beaches. Drive the scenic Road to Hana, watch the sunrise from Haleakala Crater, or snorkel in Molokini Crater. Enjoy water sports, whale watching, and the island's relaxed vibes.", + "tags": ["Island", "Road trip", "Beach"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/maui.jpg", + "location": { + "latitude": 20.7984, + "longitude": -156.3319 + } + }, + { + "ref": "milford-sound", + "name": "Milford Sound", + "country": "New Zealand", + "continent": "Oceania", + "knownFor": "Milford Sound, nestled in Fiordland National Park, offers breathtaking landscapes with towering cliffs, cascading waterfalls, and pristine waters. Visitors can cruise the fiord, kayak among the peaks, or hike the Milford Track for a multi-day adventure. The area is also known for its rich biodiversity, including dolphins, seals, and penguins.", + "tags": ["Secluded", "Hiking", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/milford-sound.jpg", + "location": { + "latitude": -44.6717, + "longitude": 167.9260 + } + }, + { + "ref": "moab", + "name": "Moab", + "country": "United States", + "continent": "North America", + "knownFor": "Moab is an adventurer's paradise, renowned for its stunning red rock formations and world-class outdoor activities. Hiking, mountain biking, and off-roading are popular pursuits in Arches and Canyonlands National Parks. The Colorado River offers white-water rafting and kayaking, while the surrounding desert landscapes provide endless opportunities for exploration and discovery.", + "tags": ["Desert", "Adventure sports", "Off-the-beaten-path"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/moab.jpg", + "location": { + "latitude": 38.5733, + "longitude": -109.5498 + } + }, + { + "ref": "montenegro", + "name": "Montenegro", + "country": "Montenegro", + "continent": "Europe", + "knownFor": "Montenegro is a small Balkan country with a dramatic coastline, soaring mountains, and charming towns. Explore the Bay of Kotor, a UNESCO World Heritage Site, hike in Durmitor National Park, or relax on the beaches of Budva. Discover the historic cities of Kotor and Cetinje and enjoy the local seafood specialties.", + "tags": ["Beach", "Mountain", "Historic"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/montenegro.jpg", + "location": { + "latitude": 42.7087, + "longitude": 19.3744 + } + }, + { + "ref": "mosel-valley", + "name": "Mosel Valley", + "country": "Germany", + "continent": "Europe", + "knownFor": "Embark on a journey through picturesque vineyards and charming villages along the Mosel River. Explore medieval castles, indulge in world-renowned Riesling wines, and savor authentic German cuisine. Hike or bike along scenic trails, or take a leisurely river cruise to soak in the breathtaking landscapes.", + "tags": ["River", "Wine tasting", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/mosel-valley.jpg", + "location": { + "latitude": 50.0517, + "longitude": 7.2298 + } + }, + { + "ref": "myanmar", + "name": "Myanmar", + "country": "Myanmar", + "continent": "Asia", + "knownFor": "Myanmar (formerly Burma) is a country rich in culture and history, with ancient temples, stunning landscapes, and friendly people. Explore the iconic Shwedagon Pagoda in Yangon, visit the temple complex of Bagan, and cruise along the Irrawaddy River.", + "tags": ["Secluded", "Cultural experiences", "Off-the-beaten-path"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/myanmar.jpg", + "location": { + "latitude": 21.9162, + "longitude": 95.9560 + } + }, + { + "ref": "mykonos", + "name": "Mykonos", + "country": "Greece", + "continent": "Europe", + "knownFor": "Mykonos is a glamorous Greek island famous for its whitewashed houses, iconic windmills, and vibrant nightlife. Visitors can relax on beautiful beaches, explore charming towns, and indulge in delicious Greek cuisine. Mykonos is also known for its luxury hotels, designer boutiques, and lively beach clubs.", + "tags": ["Island", "Beach", "Nightlife"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/mykonos.jpg", + "location": { + "latitude": 37.4467, + "longitude": 25.3289 + } + }, + { + "ref": "nairobi", + "name": "Nairobi", + "country": "Kenya", + "continent": "Africa", + "knownFor": "Nairobi, the bustling capital of Kenya, serves as a gateway to the country's renowned safari destinations. Visit the David Sheldrick Elephant Orphanage, explore the Karen Blixen Museum, and experience the vibrant nightlife and cultural scene.", + "tags": ["City", "Wildlife watching", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/nairobi.jpg", + "location": { + "latitude": -1.2921, + "longitude": 36.8219 + } + }, + { + "ref": "namibia", + "name": "Sossusvlei", + "country": "Namibia", + "continent": "Africa", + "knownFor": "Sossusvlei is a mesmerizing salt and clay pan surrounded by towering red sand dunes in the Namib Desert. Visitors can embark on scenic drives, climb the dunes for panoramic views, and capture breathtaking photos of the unique landscape. Sossusvlei is a photographer's paradise and a must-visit for desert enthusiasts.", + "tags": ["Desert", "Off-the-beaten-path", "Photography"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/namibia.jpg", + "location": { + "latitude": -24.7380, + "longitude": 15.3426 + } + }, + { + "ref": "napa-valley", + "name": "Napa Valley", + "country": "United States", + "continent": "North America", + "knownFor": "Indulge in the world-renowned wine region of Napa Valley, California. Visit picturesque vineyards, sample exquisite wines, and savor gourmet cuisine at Michelin-starred restaurants. Explore charming towns like St. Helena and Yountville, or relax in luxurious spa resorts. Napa Valley offers a sophisticated and relaxing getaway for wine lovers and foodies.", + "tags": ["Wine tasting", "Food tours", "Luxury"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/napa-valley.jpg", + "location": { + "latitude": 38.5024, + "longitude": -122.2654 + } + }, + { + "ref": "new-orleans", + "name": "New Orleans", + "country": "United States", + "continent": "North America", + "knownFor": "New Orleans is a vibrant city with a unique culture, known for its jazz music, Mardi Gras celebrations, and Creole cuisine. Explore the French Quarter, listen to live music on Frenchmen Street, or visit the historic cemeteries. Experience the nightlife, enjoy the local food, and immerse yourself in the spirit of New Orleans.", + "tags": ["City", "Cultural experiences", "Foodie"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/new-orleans.jpg", + "location": { + "latitude": 29.9511, + "longitude": -90.0715 + } + }, + { + "ref": "new-zealand", + "name": "New Zealand", + "country": "New Zealand", + "continent": "Oceania", + "knownFor": "Embark on an adventure in New Zealand, a land of diverse landscapes, from snow-capped mountains and glaciers to geothermal wonders and lush rainforests. Hike through Fiordland National Park, explore the Waitomo Caves, and experience the thrill of bungee jumping and white-water rafting. Discover the Maori culture, indulge in delicious local cuisine, and marvel at the country's natural beauty.", + "tags": ["Adventure sports", "Hiking", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/new-zealand.jpg", + "location": { + "latitude": -40.9006, + "longitude": 174.8860 + } + }, + { + "ref": "newfoundland", + "name": "Newfoundland", + "country": "Canada", + "continent": "North America", + "knownFor": "Newfoundland boasts rugged coastlines, charming fishing villages, and abundant wildlife. Hike along scenic trails, go whale watching, and experience the unique local culture. The island's friendly people and lively music scene add to its appeal.", + "tags": ["Island", "Wildlife watching", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/newfoundland.jpg", + "location": { + "latitude": 53.1355, + "longitude": -57.6604 + } + }, + { + "ref": "nicaragua", + "name": "Nicaragua", + "country": "Nicaragua", + "continent": "North America", + "knownFor": "Nicaragua offers a diverse landscape of volcanoes, lakes, beaches, and rainforests. Visitors can enjoy adventure activities like surfing, volcano boarding, and zip-lining, as well as exploring colonial cities and experiencing the rich Nicaraguan culture.", + "tags": ["Adventure sports", "Beach", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/nicaragua.jpg", + "location": { + "latitude": 12.8654, + "longitude": -85.2072 + } + }, + { + "ref": "northern-territory", + "name": "Kakadu National Park", + "country": "Australia", + "continent": "Oceania", + "knownFor": "Kakadu National Park is a UNESCO World Heritage site with diverse landscapes, including wetlands, sandstone escarpments, and ancient rock art sites. Visitors can explore Aboriginal culture, go bird watching, take boat tours through wetlands, and admire cascading waterfalls. Kakadu is a haven for wildlife, with crocodiles, wallabies, and numerous bird species.", + "tags": ["National Park", "Wildlife watching", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/northern-territory.jpg", + "location": { + "latitude": -13.0923, + "longitude": 132.3938 + } + }, + { + "ref": "oaxaca", + "name": "Oaxaca", + "country": "Mexico", + "continent": "North America", + "knownFor": "Immerse yourself in the vibrant culture and rich history of Oaxaca, Mexico. Explore ancient Zapotec ruins, visit colorful markets filled with handicrafts, and experience traditional festivals. Sample the region's renowned cuisine, including mole sauces and mezcal. Oaxaca offers a unique and authentic travel experience for culture enthusiasts and foodies.", + "tags": ["Cultural experiences", "Food tours", "Historic"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/oaxaca.jpg", + "location": { + "latitude": 17.0732, + "longitude": -96.7266 + } + }, + { + "ref": "okavango-delta", + "name": "Okavango Delta", + "country": "Botswana", + "continent": "Africa", + "knownFor": "The Okavango Delta, a vast inland delta in Botswana, is a haven for wildlife. Explore the waterways by mokoro (traditional canoe) and witness elephants, lions, hippos, and an array of bird species in their natural habitat.", + "tags": ["River", "Wildlife watching", "Adventure sports"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/okavango-delta.jpg", + "location": { + "latitude": -19.4318, + "longitude": 22.8112 + } + }, + { + "ref": "oman", + "name": "Oman", + "country": "Oman", + "continent": "Asia", + "knownFor": "Oman, a country on the Arabian Peninsula, is a hidden gem with dramatic landscapes, ancient forts, and warm hospitality. Explore the vast deserts, swim in turquoise wadis, and hike through rugged mountains. Visit traditional souks, experience Bedouin culture, and discover the unique blend of modernity and tradition in this captivating destination.", + "tags": ["Desert", "Secluded", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/oman.jpg", + "location": { + "latitude": 21.5126, + "longitude": 55.9233 + } + }, + { + "ref": "pagos-islands", + "name": "Galápagos Islands", + "country": "Ecuador", + "continent": "South America", + "knownFor": "A unique archipelago off the coast of Ecuador, the Galápagos Islands is a haven for wildlife enthusiasts. Encounter giant tortoises, marine iguanas, blue-footed boobies, and sea lions in their natural habitat. Snorkeling and diving opportunities reveal a vibrant underwater world.", + "tags": ["Island", "Wildlife watching", "Snorkeling"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/pagos-islands.jpg", + "location": { + "latitude": -0.7399, + "longitude": -90.3126 + } + }, + { + "ref": "patagonia", + "name": "Patagonia", + "country": "Argentina and Chile", + "continent": "South America", + "knownFor": "Patagonia is a vast region at the southern tip of South America, known for its glaciers, mountains, and diverse wildlife. Visitors can embark on trekking adventures, witness the Perito Moreno Glacier, go kayaking, and spot penguins, whales, and other animals.", + "tags": ["Hiking", "Adventure sports", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/patagonia.jpg", + "location": { + "latitude": -50.9426, + "longitude": -73.4130 + } + }, + { + "ref": "peru", + "name": "Arequipa", + "country": "Peru", + "continent": "South America", + "knownFor": "Arequipa, known as the \"White City\" due to its buildings made of white volcanic stone, is a beautiful colonial city in southern Peru. Visitors can explore the historic center, visit the Santa Catalina Monastery, and enjoy stunning views of the surrounding volcanoes. Arequipa is also a gateway to the Colca Canyon, one of the deepest canyons in the world.", + "tags": ["City", "Historic", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/peru.jpg", + "location": { + "latitude": -16.4090, + "longitude": -71.5375 + } + }, + { + "ref": "peruvian-amazon", + "name": "Peruvian Amazon", + "country": "Peru", + "continent": "South America", + "knownFor": "Venture into the heart of the Amazon rainforest, a biodiversity hotspot teeming with exotic wildlife and indigenous cultures. Embark on jungle treks, navigate the Amazon River, and spot unique flora and fauna. Experience the thrill of adventure travel and connect with nature in its purest form.", + "tags": ["Jungle", "Adventure sports", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/peruvian-amazon.jpg", + "location": { + "latitude": -3.7491, + "longitude": -73.2538 + } + }, + { + "ref": "phnom-penh", + "name": "Phnom Penh", + "country": "Cambodia", + "continent": "Asia", + "knownFor": "Immerse yourself in the vibrant culture and rich history of Phnom Penh, Cambodia's bustling capital. Visit the Royal Palace, explore ancient temples like Wat Phnom, and delve into the poignant history at the Tuol Sleng Genocide Museum. Experience the city's energetic nightlife and savor delicious Khmer cuisine.", + "tags": ["City", "Historic", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/phnom-penh.jpg", + "location": { + "latitude": 11.5564, + "longitude": 104.9282 + } + }, + { + "ref": "phuket", + "name": "Phuket", + "country": "Thailand", + "continent": "Asia", + "knownFor": "Relax on the stunning beaches of Phuket, Thailand's largest island. Explore the turquoise waters of the Andaman Sea, go snorkeling or scuba diving among coral reefs, or visit nearby islands like Phi Phi. Enjoy the vibrant nightlife, indulge in Thai massages, and experience the local culture. Phuket offers a perfect blend of relaxation and adventure for beach lovers and those seeking a tropical escape.", + "tags": ["Beach", "Island", "Snorkeling"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/phuket.jpg", + "location": { + "latitude": 7.9519, + "longitude": 98.3364 + } + }, + { + "ref": "positano", + "name": "Positano", + "country": "Italy", + "continent": "Europe", + "knownFor": "Nestled along the Amalfi Coast, Positano enchants with its colorful cliffside houses, pebble beaches, and luxury boutiques. Explore the narrow streets, indulge in delicious Italian cuisine, and soak up the romantic atmosphere.", + "tags": ["Beach", "Romantic", "Luxury"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/positano.jpg", + "location": { + "latitude": 40.6280, + "longitude": 14.4857 + } + }, + { + "ref": "prague", + "name": "Prague", + "country": "Czech Republic", + "continent": "Europe", + "knownFor": "Prague, with its stunning architecture and rich history, is a fairytale city. Explore the Prague Castle, wander through the Old Town Square, and enjoy a traditional Czech meal. The city's charming atmosphere and affordable prices make it a popular destination.", + "tags": ["City", "Historic", "Sightseeing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/prague.jpg", + "location": { + "latitude": 50.0755, + "longitude": 14.4378 + } + }, + { + "ref": "puerto-rico", + "name": "Puerto Rico", + "country": "United States", + "continent": "North America", + "knownFor": "Puerto Rico is a Caribbean island with a rich history, vibrant culture, and stunning natural beauty. Explore the historic forts of Old San Juan, relax on the beaches of Vieques, or hike in El Yunque National Forest. Experience the bioluminescent bays, go salsa dancing, and enjoy the local cuisine.", + "tags": ["Island", "Beach", "Historic"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/puerto-rico.jpg", + "location": { + "latitude": 18.2208, + "longitude": -66.5901 + } + }, + { + "ref": "punta-cana", + "name": "Punta Cana", + "country": "Dominican Republic", + "continent": "North America", + "knownFor": "Punta Cana is a renowned beach destination with pristine white sand, crystal-clear waters, and luxurious resorts. Enjoy water sports, golf, or simply unwind by the ocean and experience the vibrant Dominican culture.", + "tags": ["Beach", "All-inclusive", "Family-friendly"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/punta-cana.jpg", + "location": { + "latitude": 18.5823, + "longitude": -68.4048 + } + }, + { + "ref": "quebec-city", + "name": "Quebec City", + "country": "Canada", + "continent": "North America", + "knownFor": "Experience the European charm of Quebec City, a UNESCO World Heritage Site. Wander through the historic Old Town with its cobblestone streets and fortified walls, visit the iconic Chateau Frontenac, and admire the stunning views of the St. Lawrence River. Quebec City offers a unique blend of history, culture, and French Canadian charm for a memorable getaway.", + "tags": ["City", "Historic", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/quebec-city.jpg", + "location": { + "latitude": 46.8139, + "longitude": -71.2082 + } + }, + { + "ref": "queenstown", + "name": "Queenstown", + "country": "New Zealand", + "continent": "Oceania", + "knownFor": "Experience the adventure capital of the world in Queenstown, New Zealand. Surrounded by stunning mountains and Lake Wakatipu, this town offers everything from bungy jumping and skydiving to skiing and hiking.", + "tags": ["Adventure sports", "Lake", "Mountain"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/queenstown.jpg", + "location": { + "latitude": -45.0312, + "longitude": 168.6626 + } + }, + { + "ref": "rajasthan", + "name": "Rajasthan", + "country": "India", + "continent": "Asia", + "knownFor": "Rajasthan, a state in northwestern India, is known for its opulent palaces, vibrant culture, and desert landscapes. Visitors can explore the cities of Jaipur, Jodhpur, and Udaipur, with their magnificent forts and palaces. The region also offers camel safaris, desert camping, and opportunities to experience traditional Rajasthani music and dance.", + "tags": ["Historic", "Desert", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/rajasthan.jpg", + "location": { + "latitude": 27.0238, + "longitude": 74.2179 + } + }, + { + "ref": "reunion-island", + "name": "Réunion Island", + "country": "France", + "continent": "Africa", + "knownFor": "This volcanic island boasts dramatic landscapes, including Piton de la Fournaise, one of the world's most active volcanoes. Hike through lush rainforests, relax on black sand beaches, and experience the unique Creole culture.", + "tags": ["Secluded", "Hiking", "Tropical"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/reunion-island.jpg", + "location": { + "latitude": -21.1151, + "longitude": 55.5364 + } + }, + { + "ref": "rio-de-janeiro", + "name": "Rio de Janeiro", + "country": "Brazil", + "continent": "South America", + "knownFor": "Rio de Janeiro is a vibrant city known for its stunning beaches, iconic landmarks like Christ the Redeemer and Sugarloaf Mountain, and lively Carnival celebrations. Visitors can enjoy sunbathing, surfing, and exploring the city's diverse neighborhoods, experiencing the infectious energy and cultural richness of Brazil.", + "tags": ["City", "Beach", "Party"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/rio-de-janeiro.jpg", + "location": { + "latitude": -22.9068, + "longitude": -43.1729 + } + }, + { + "ref": "salar-de-uyuni", + "name": "Salar de Uyuni", + "country": "Bolivia", + "continent": "South America", + "knownFor": "Witness the surreal beauty of the world's largest salt flats, a mesmerizing landscape that transforms into a giant mirror during the rainy season. Capture incredible photos, explore unique rock formations, and visit nearby lagoons teeming with flamingos.", + "tags": ["Desert", "Off-the-beaten-path", "Photography"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/salar-de-uyuni.jpg", + "location": { + "latitude": -20.2163, + "longitude": -67.4891 + } + }, + { + "ref": "san-diego", + "name": "San Diego", + "country": "United States", + "continent": "North America", + "knownFor": "San Diego, a sunny coastal city in Southern California, boasts beautiful beaches, a world-famous zoo, and a vibrant cultural scene. Explore Balboa Park, visit the historic Gaslamp Quarter, and enjoy water sports along the Pacific coast.", + "tags": ["City", "Beach", "Family-friendly"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/san-diego.jpg", + "location": { + "latitude": 32.7157, + "longitude": -117.1611 + } + }, + { + "ref": "san-miguel-de-allende", + "name": "San Miguel de Allende", + "country": "Mexico", + "continent": "North America", + "knownFor": "This colonial city in central Mexico is a UNESCO World Heritage site with stunning Spanish architecture, vibrant cultural events, and a thriving arts scene. Visitors can explore historic churches, wander through cobbled streets lined with colorful houses, and enjoy delicious Mexican cuisine. San Miguel de Allende is also a popular destination for art classes and workshops.", + "tags": ["City", "Historic", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/san-miguel-de-allende.jpg", + "location": { + "latitude": 20.9144, + "longitude": -100.7430 + } + }, + { + "ref": "san-sebastian", + "name": "San Sebastian", + "country": "Spain", + "continent": "Europe", + "knownFor": "Indulge in the culinary delights of this coastal paradise, renowned for its Michelin-starred restaurants and pintxos bars. Relax on the beautiful beaches, explore the charming Old Town, and hike or bike in the surrounding hills. Experience the vibrant culture and lively festivals of the Basque region.", + "tags": ["Beach", "City", "Foodie"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/san-sebastian.jpg", + "location": { + "latitude": 43.3183, + "longitude": -1.9812 + } + }, + { + "ref": "santorini", + "name": "Santorini", + "country": "Greece", + "continent": "Europe", + "knownFor": "Santorini's iconic whitewashed villages perched on volcanic cliffs offer breathtaking views of the Aegean Sea. Explore charming Oia, visit ancient Akrotiri, and enjoy romantic sunsets with caldera views.", + "tags": ["Island", "Romantic", "Luxury"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/santorini.jpg", + "location": { + "latitude": 36.3932, + "longitude": 25.4615 + } + }, + { + "ref": "sardinia", + "name": "Sardinia", + "country": "Italy", + "continent": "Europe", + "knownFor": "Experience the allure of Sardinia, a Mediterranean island boasting stunning coastlines, turquoise waters, and rugged mountains. Explore charming villages, ancient ruins, and secluded coves. Indulge in delicious Sardinian cuisine, hike scenic trails, and discover the island's rich history and culture.", + "tags": ["Island", "Beach", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/sardinia.jpg", + "location": { + "latitude": 40.1209, + "longitude": 9.0129 + } + }, + { + "ref": "scotland", + "name": "Scotland", + "country": "United Kingdom", + "continent": "Europe", + "knownFor": "Scotland is known for its dramatic landscapes, historic castles, and vibrant cities. Explore the Scottish Highlands, visit Edinburgh Castle, and sample Scotch whisky. The country's rich history and culture, along with its friendly people, make it a captivating destination.", + "tags": ["Mountain", "Historic", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/scotland.jpg", + "location": { + "latitude": 56.4907, + "longitude": -4.2026 + } + }, + { + "ref": "scottish-highlands", + "name": "Scottish Highlands", + "country": "Scotland", + "continent": "Europe", + "knownFor": "The Scottish Highlands, a mountainous region in northern Scotland, is renowned for its rugged beauty, ancient castles, and rich history. Visitors can explore the dramatic landscapes through hiking, climbing, and scenic drives, or visit historic sites like Loch Ness and Eilean Donan Castle. The region is also famous for its whisky distilleries and traditional Highland culture.", + "tags": ["Mountain", "Historic", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/scottish-highlands.jpg", + "location": { + "latitude": 57.3333, + "longitude": -4.7167 + } + }, + { + "ref": "seoul", + "name": "Seoul", + "country": "South Korea", + "continent": "Asia", + "knownFor": "Seoul is a vibrant metropolis blending modern skyscrapers with ancient palaces and temples. Visitors can explore historical landmarks, experience K-pop culture, indulge in delicious Korean cuisine, and enjoy the city's bustling nightlife.", + "tags": ["City", "Cultural experiences", "Nightlife"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/seoul.jpg", + "location": { + "latitude": 37.5665, + "longitude": 126.9780 + } + }, + { + "ref": "serengeti-national-park", + "name": "Serengeti National Park", + "country": "Tanzania", + "continent": "Africa", + "knownFor": "Serengeti National Park is renowned for its incredible wildlife and the annual Great Migration, where millions of wildebeest, zebras, and other animals traverse the plains in search of fresh grazing. Visitors can embark on thrilling safari adventures, witness predator-prey interactions, and marvel at the diversity of the African savanna.", + "tags": ["Wildlife watching", "Safari", "Adventure"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/serengeti-national-park.jpg", + "location": { + "latitude": -2.3328, + "longitude": 34.8253 + } + }, + { + "ref": "seville", + "name": "Seville", + "country": "Spain", + "continent": "Europe", + "knownFor": "Seville, the vibrant capital of Andalusia, is renowned for its flamenco dancing, Moorish architecture, and lively tapas bars. Explore the stunning Alcázar palace, witness a passionate flamenco performance, and wander through the charming Santa Cruz district.", + "tags": ["City", "Cultural experiences", "Food tours"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/seville.jpg", + "location": { + "latitude": 37.3891, + "longitude": -5.9845 + } + }, + { + "ref": "singapore", + "name": "Singapore", + "country": "Singapore", + "continent": "Asia", + "knownFor": "Experience a vibrant mix of cultures, cutting-edge architecture, and lush green spaces in this dynamic city-state. Discover futuristic Gardens by the Bay, indulge in diverse culinary delights, and explore world-class shopping and entertainment options.", + "tags": ["City", "Cultural experiences", "Foodie"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/singapore.jpg", + "location": { + "latitude": 1.3521, + "longitude": 103.8198 + } + }, + { + "ref": "slovenia", + "name": "Slovenia", + "country": "Slovenia", + "continent": "Europe", + "knownFor": "Slovenia is a small country in Central Europe with stunning alpine scenery, charming towns, and a rich history. Visit Lake Bled, a picturesque lake with a church on an island, explore the Postojna Cave, or hike in Triglav National Park. Discover the capital city of Ljubljana, enjoy the local wines, and experience the Slovenian hospitality.", + "tags": ["Lake", "Mountain", "City"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/slovenia.jpg", + "location": { + "latitude": 46.1512, + "longitude": 14.9955 + } + }, + { + "ref": "sri-lanka", + "name": "Sri Lanka", + "country": "Sri Lanka", + "continent": "Asia", + "knownFor": "Sri Lanka is an island nation off the southern coast of India, known for its ancient ruins, beautiful beaches, and diverse wildlife. Visit the Sigiriya rock fortress, relax on the beaches of Bentota, or go on a safari in Yala National Park. Explore the tea plantations, experience the local culture, and enjoy the delicious Sri Lankan cuisine.", + "tags": ["Island", "Beach", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/sri-lanka.jpg", + "location": { + "latitude": 7.8731, + "longitude": 80.7718 + } + }, + { + "ref": "svalbard", + "name": "Svalbard", + "country": "Norway", + "continent": "Europe", + "knownFor": "Svalbard, an Arctic archipelago under Norwegian sovereignty, is a remote and captivating destination for adventurers and nature enthusiasts. Witness glaciers, fjords, and ice-covered landscapes. Spot polar bears, walruses, and reindeer, and experience the midnight sun or the northern lights. Svalbard offers a unique opportunity to explore the Arctic wilderness.", + "tags": ["Off-the-beaten-path", "Wildlife watching", "Winter destination"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/svalbard.jpg", + "location": { + "latitude": 77.8750, + "longitude": 20.9752 + } + }, + { + "ref": "swiss-alps", + "name": "Swiss Alps", + "country": "Switzerland", + "continent": "Europe", + "knownFor": "Discover the breathtaking beauty of the Swiss Alps, a paradise for outdoor enthusiasts. Hike through scenic mountain trails, go skiing or snowboarding in world-class resorts, or take a scenic train ride through the mountains. Enjoy the fresh air, charming villages, and stunning scenery. The Swiss Alps offer an unforgettable experience for nature lovers and adventure seekers.", + "tags": ["Mountain", "Hiking", "Skiing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/swiss-alps.jpg", + "location": { + "latitude": 46.8182, + "longitude": 8.2275 + } + }, + { + "ref": "tasmania", + "name": "Tasmania", + "country": "Australia", + "continent": "Oceania", + "knownFor": "Discover the wild beauty of Tasmania, an island state off the coast of Australia. Explore Cradle Mountain-Lake St Clair National Park, with its rugged mountains and pristine lakes. Visit Port Arthur Historic Site, a former penal colony, or encounter unique wildlife like Tasmanian devils and quolls.", + "tags": ["Island", "Hiking", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/tasmania.jpg", + "location": { + "latitude": -41.4545, + "longitude": 145.9707 + } + }, + { + "ref": "tel-aviv", + "name": "Tel Aviv", + "country": "Israel", + "continent": "Asia", + "knownFor": "Experience the vibrant and cosmopolitan city of Tel Aviv, known for its beaches, Bauhaus architecture, and thriving nightlife. Relax on the sandy shores of the Mediterranean Sea, explore the trendy neighborhoods of Neve Tzedek and Florentin, and enjoy the city's diverse culinary scene. Tel Aviv offers a perfect blend of beach relaxation, cultural experiences, and exciting nightlife.", + "tags": ["City", "Beach", "Nightlife"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/tel-aviv.jpg", + "location": { + "latitude": 32.0853, + "longitude": 34.7818 + } + }, + { + "ref": "trans-siberian-railway", + "name": "Trans-Siberian Railway", + "country": "Russia", + "continent": "Asia", + "knownFor": "The Trans-Siberian Railway is the longest railway line in the world, stretching over 9,000 kilometers from Moscow to Vladivostok. This epic journey offers travelers a unique opportunity to experience the vastness and diversity of Russia, passing through bustling cities, remote villages, and stunning natural landscapes.", + "tags": ["Adventure sports", "Cultural experiences", "Long-haul vacation"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/trans-siberian-railway.jpg", + "location": { + "latitude": 55.0500, + "longitude": 82.9500 + } + }, + { + "ref": "transylvania", + "name": "Transylvania", + "country": "Romania", + "continent": "Europe", + "knownFor": "Transylvania, a region in Romania, is famous for its medieval towns, fortified churches, and stunning Carpathian Mountain scenery. Visitors can explore Bran Castle, associated with the Dracula legend, visit historic cities like Brasov and Sibiu, and hike or ski in the mountains. The region also offers opportunities to experience traditional Romanian culture and cuisine.", + "tags": ["Historic", "Mountain", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/transylvania.jpg", + "location": { + "latitude": 46.3591, + "longitude": 25.0106 + } + }, + { + "ref": "tulum", + "name": "Tulum", + "country": "Mexico", + "continent": "North America", + "knownFor": "Tulum seamlessly blends ancient Mayan history with modern bohemian vibes. Visitors can explore the Tulum Archaeological Site, perched on cliffs overlooking the Caribbean Sea, and discover well-preserved ruins. Pristine beaches offer relaxation and water activities, while the town's eco-chic atmosphere provides yoga retreats, wellness centers, and sustainable dining options.", + "tags": ["Beach", "Cultural experiences", "Wellness retreats"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/tulum.jpg", + "location": { + "latitude": 20.2114, + "longitude": -87.4654 + } + }, + { + "ref": "turkish-riviera", + "name": "Turkish Riviera", + "country": "Turkey", + "continent": "Asia", + "knownFor": "The Turkish Riviera offers a mix of ancient ruins, stunning beaches, and turquoise waters. Explore the historical sites of Ephesus and Antalya, relax on the sandy shores, and enjoy water sports like sailing and snorkeling. The region's delicious cuisine and affordable prices add to its appeal.", + "tags": ["Beach", "Historic", "Sightseeing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/turkish-riviera.jpg", + "location": { + "latitude": 36.8969, + "longitude": 30.7133 + } + }, + { + "ref": "tuscany", + "name": "Tuscany", + "country": "Italy", + "continent": "Europe", + "knownFor": "Explore the rolling hills and vineyards of Tuscany, indulging in wine tastings and farm-to-table cuisine. Discover charming medieval towns, Renaissance art, and historic cities like Florence and Siena. Immerse yourself in the region's rich culture and art scene, or simply relax and soak up the idyllic scenery.", + "tags": ["Cultural experiences", "Food tours", "Wine tasting"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/tuscany.jpg", + "location": { + "latitude": 43.7711, + "longitude": 11.2486 + } + }, + { + "ref": "us-virgin-islands", + "name": "US Virgin Islands", + "country": "United States", + "continent": "North America", + "knownFor": "Escape to the Caribbean paradise of the US Virgin Islands, where you can relax on pristine beaches, explore coral reefs, and experience the laid-back island lifestyle. Visit the historic towns of Charlotte Amalie and Christiansted, go sailing or snorkeling in crystal-clear waters, or simply soak up the sun. The US Virgin Islands offer a perfect tropical getaway for beach lovers and those seeking a relaxing escape.", + "tags": ["Island", "Beach", "Relaxing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/us-virgin-islands.jpg", + "location": { + "latitude": 18.3358, + "longitude": -64.8963 + } + }, + { + "ref": "vancouver-island", + "name": "Vancouver Island", + "country": "Canada", + "continent": "North America", + "knownFor": "Vancouver Island, located off Canada's Pacific coast, is a haven for nature lovers and adventure seekers. Explore the rugged coastline, ancient rainforests, and snow-capped mountains. Go whale watching, kayaking, or surfing, and discover charming towns and vibrant cities like Victoria. Vancouver Island offers a perfect blend of wilderness and urban experiences.", + "tags": ["Island", "Adventure sports", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/vancouver-island.jpg", + "location": { + "latitude": 49.6506, + "longitude": -125.4498 + } + }, + { + "ref": "vienna", + "name": "Vienna", + "country": "Austria", + "continent": "Europe", + "knownFor": "Step into the imperial city of Vienna, where grand palaces, historical landmarks, and elegant cafes exude charm and sophistication. Explore museums, art galleries, and renowned opera houses, or visit Schönbrunn Palace and delve into Habsburg history. Enjoy classical music concerts, indulge in Viennese pastries, and experience the city's rich cultural heritage.", + "tags": ["City", "Historic", "Cultural experiences"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/vienna.jpg", + "location": { + "latitude": 48.2082, + "longitude": 16.3738 + } + }, + { + "ref": "vietnam", + "name": "Vietnam", + "country": "Vietnam", + "continent": "Asia", + "knownFor": "Vietnam offers a rich tapestry of culture, history, and natural beauty. Explore the bustling streets of Hanoi, cruise through the scenic Ha Long Bay, and discover the ancient town of Hoi An. From delicious street food to stunning landscapes, Vietnam is a destination that will captivate your senses.", + "tags": ["Cultural experiences", "Food tours", "Sightseeing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/vietnam.jpg", + "location": { + "latitude": 14.0583, + "longitude": 108.2772 + } + }, + { + "ref": "western-australia", + "name": "Western Australia", + "country": "Australia", + "continent": "Australia", + "knownFor": "Western Australia, the largest state in Australia, is a land of vast landscapes, stunning coastlines, and unique wildlife. Explore the vibrant city of Perth, swim with whale sharks at Ningaloo Reef, and discover the ancient rock formations of the Kimberley region. From wineries to deserts, Western Australia offers a diverse and unforgettable experience.", + "tags": ["Beach", "Road trip destination", "Wildlife watching"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/western-australia.jpg", + "location": { + "latitude": -25.0423, + "longitude": 117.7930 + } + }, + { + "ref": "yellowstone-national-park", + "name": "Yellowstone National Park", + "country": "United States", + "continent": "North America", + "knownFor": "Witness the geothermal wonders of Yellowstone, with its geysers, hot springs, and mudpots. Observe abundant wildlife, including bison, elk, and wolves. Explore the Grand Canyon of the Yellowstone, go hiking or camping, and enjoy winter sports.", + "tags": ["National Park", "Wildlife watching", "Hiking"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/yellowstone-national-park.jpg", + "location": { + "latitude": 44.4280, + "longitude": -110.5885 + } + }, + { + "ref": "yosemite-national-park", + "name": "Yosemite National Park", + "country": "United States", + "continent": "North America", + "knownFor": "Yosemite National Park, located in California's Sierra Nevada mountains, is renowned for its towering granite cliffs, giant sequoia trees, and stunning waterfalls. Visitors can enjoy hiking, camping, rock climbing, and exploring the park's natural wonders, including Yosemite Valley, Half Dome, and Glacier Point.", + "tags": ["Mountain", "Hiking", "Sightseeing"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/yosemite-national-park.jpg", + "location": { + "latitude": 37.8651, + "longitude": -119.5383 + } + }, + { + "ref": "yucatan-peninsula", + "name": "Yucatan Peninsula", + "country": "Mexico", + "continent": "North America", + "knownFor": "Discover ancient Mayan ruins, explore vibrant coral reefs, and relax on pristine beaches in the Yucatan Peninsula. Dive into cenotes, swim with whale sharks, and experience the rich culture and history of this captivating region.", + "tags": ["Beach", "Historic", "Scuba diving"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/yucatan-peninsula.jpg", + "location": { + "latitude": 20.4220, + "longitude": -88.5689 + } + }, + { + "ref": "zanzibar", + "name": "Zanzibar", + "country": "Tanzania", + "continent": "Africa", + "knownFor": "Zanzibar is a Tanzanian archipelago off the coast of East Africa, known for its stunning beaches, turquoise waters, and historical Stone Town. Visitors can relax on the beach, explore the UNESCO-listed Stone Town, go diving or snorkeling, and experience the island's unique blend of African, Arab, and European influences.", + "tags": ["Beach", "Cultural experiences", "Scuba diving"], + "imageUrl": "https://storage.googleapis.com/tripedia-images/destinations/zanzibar.jpg", + "location": { + "latitude": -6.1659, + "longitude": 39.2026 + } + } +] \ No newline at end of file diff --git a/compass_25/assets/google_fonts/Rubik-Black.ttf b/compass_25/assets/google_fonts/Rubik-Black.ttf new file mode 100644 index 0000000..055ad22 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-Black.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-BlackItalic.ttf b/compass_25/assets/google_fonts/Rubik-BlackItalic.ttf new file mode 100644 index 0000000..308529c Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-BlackItalic.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-Bold.ttf b/compass_25/assets/google_fonts/Rubik-Bold.ttf new file mode 100644 index 0000000..1a9693d Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-Bold.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-BoldItalic.ttf b/compass_25/assets/google_fonts/Rubik-BoldItalic.ttf new file mode 100644 index 0000000..abf7604 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-BoldItalic.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-ExtraBold.ttf b/compass_25/assets/google_fonts/Rubik-ExtraBold.ttf new file mode 100644 index 0000000..3b1e190 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-ExtraBold.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-ExtraBoldItalic.ttf b/compass_25/assets/google_fonts/Rubik-ExtraBoldItalic.ttf new file mode 100644 index 0000000..59cd758 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-ExtraBoldItalic.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-Italic.ttf b/compass_25/assets/google_fonts/Rubik-Italic.ttf new file mode 100644 index 0000000..1683a76 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-Italic.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-Light.ttf b/compass_25/assets/google_fonts/Rubik-Light.ttf new file mode 100644 index 0000000..8a5a50a Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-Light.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-LightItalic.ttf b/compass_25/assets/google_fonts/Rubik-LightItalic.ttf new file mode 100644 index 0000000..b028d93 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-LightItalic.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-Medium.ttf b/compass_25/assets/google_fonts/Rubik-Medium.ttf new file mode 100644 index 0000000..f0bd595 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-Medium.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-MediumItalic.ttf b/compass_25/assets/google_fonts/Rubik-MediumItalic.ttf new file mode 100644 index 0000000..1a7d7f9 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-MediumItalic.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-Regular.ttf b/compass_25/assets/google_fonts/Rubik-Regular.ttf new file mode 100644 index 0000000..8b7b632 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-Regular.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-SemiBold.ttf b/compass_25/assets/google_fonts/Rubik-SemiBold.ttf new file mode 100644 index 0000000..26f657d Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-SemiBold.ttf differ diff --git a/compass_25/assets/google_fonts/Rubik-SemiBoldItalic.ttf b/compass_25/assets/google_fonts/Rubik-SemiBoldItalic.ttf new file mode 100644 index 0000000..8872983 Binary files /dev/null and b/compass_25/assets/google_fonts/Rubik-SemiBoldItalic.ttf differ diff --git a/compass_25/assets/icon.png b/compass_25/assets/icon.png new file mode 100644 index 0000000..d27b526 Binary files /dev/null and b/compass_25/assets/icon.png differ diff --git a/compass_25/assets/icons/chat.png b/compass_25/assets/icons/chat.png new file mode 100644 index 0000000..418fa8c Binary files /dev/null and b/compass_25/assets/icons/chat.png differ diff --git a/compass_25/assets/icons/directions_run.png b/compass_25/assets/icons/directions_run.png new file mode 100644 index 0000000..4e52f61 Binary files /dev/null and b/compass_25/assets/icons/directions_run.png differ diff --git a/compass_25/assets/icons/explore.png b/compass_25/assets/icons/explore.png new file mode 100644 index 0000000..acc945c Binary files /dev/null and b/compass_25/assets/icons/explore.png differ diff --git a/compass_25/assets/icons/search.png b/compass_25/assets/icons/search.png new file mode 100644 index 0000000..3012bc1 Binary files /dev/null and b/compass_25/assets/icons/search.png differ diff --git a/compass_25/assets/icons/user.png b/compass_25/assets/icons/user.png new file mode 100644 index 0000000..7ba057a Binary files /dev/null and b/compass_25/assets/icons/user.png differ diff --git a/compass_25/assets/login_bg.png b/compass_25/assets/login_bg.png new file mode 100644 index 0000000..10b00d4 Binary files /dev/null and b/compass_25/assets/login_bg.png differ diff --git a/compass_25/assets/logo.png b/compass_25/assets/logo.png new file mode 100644 index 0000000..2291222 Binary files /dev/null and b/compass_25/assets/logo.png differ diff --git a/compass_25/assets/logotype.png b/compass_25/assets/logotype.png new file mode 100644 index 0000000..0349372 Binary files /dev/null and b/compass_25/assets/logotype.png differ diff --git a/compass_25/assets/title.png b/compass_25/assets/title.png new file mode 100644 index 0000000..82c6601 Binary files /dev/null and b/compass_25/assets/title.png differ diff --git a/compass_25/assets/user.jpg b/compass_25/assets/user.jpg new file mode 100644 index 0000000..1e79068 Binary files /dev/null and b/compass_25/assets/user.jpg differ diff --git a/compass_25/devtools_options.yaml b/compass_25/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/compass_25/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/compass_25/docs/walkthrough.gif b/compass_25/docs/walkthrough.gif new file mode 100644 index 0000000..9bda324 Binary files /dev/null and b/compass_25/docs/walkthrough.gif differ diff --git a/compass_25/firebase.json b/compass_25/firebase.json new file mode 100644 index 0000000..d2cd203 --- /dev/null +++ b/compass_25/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"compass-ios-455022","appId":"1:1025902230344:android:5623a28d48302226b6117d","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"compass-ios-455022","appId":"1:1025902230344:ios:a0ce366519e0fc27b6117d","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"compass-ios-455022","configurations":{"android":"1:1025902230344:android:5623a28d48302226b6117d","ios":"1:1025902230344:ios:a0ce366519e0fc27b6117d","web":"1:1025902230344:web:eb9ca50d538d3419b6117d"}}}}}} \ No newline at end of file diff --git a/compass_25/firepit-log.txt b/compass_25/firepit-log.txt new file mode 100644 index 0000000..7df0ef5 --- /dev/null +++ b/compass_25/firepit-log.txt @@ -0,0 +1,25 @@ +Welcome to firepit v1.1.0! +Doing JSON parses for version checks at /snapshot/firepit/vendor/node_modules/firebase-tools/package.json +firebase-tools +Installed ft@10.5.0 and packaged ft@10.5.0 +Checking for npm/bin/npm-cli install at /Users/ryjohn/.cache/firebase/tools/lib/node_modules/npm/bin/npm-cli +Checking for npm/bin/npm-cli install at /Users/ryjohn/.cache/firebase/tools/node_modules/npm/bin/npm-cli +Checking for npm/bin/npm-cli install at /snapshot/firepit/node_modules/npm/bin/npm-cli +Found npm/bin/npm-cli install. +Checking for npm/bin/npm-cli install at /Users/ryjohn/.cache/firebase/tools/lib/node_modules/npm/bin/npm-cli +Checking for npm/bin/npm-cli install at /Users/ryjohn/.cache/firebase/tools/node_modules/npm/bin/npm-cli +Checking for npm/bin/npm-cli install at /snapshot/firepit/node_modules/npm/bin/npm-cli +Found npm/bin/npm-cli install. +ShellJSInternalError: ENOENT: no such file or directory, unlink '/Users/ryjohn/.cache/firebase/runtime/node.bat' +Runtime binaries created. +/Users/ryjohn/.bin/firebase +/Users/ryjohn/.bin/firebase,/snapshot/firepit/firepit.js,--version +Checking for npm/bin/npm-cli install at /Users/ryjohn/.cache/firebase/tools/lib/node_modules/npm/bin/npm-cli +Checking for npm/bin/npm-cli install at /Users/ryjohn/.cache/firebase/tools/node_modules/npm/bin/npm-cli +Checking for npm/bin/npm-cli install at /snapshot/firepit/node_modules/npm/bin/npm-cli +Found npm/bin/npm-cli install. +Checking for npm/bin/npm-cli install at /Users/ryjohn/.cache/firebase/tools/lib/node_modules/npm/bin/npm-cli +Checking for npm/bin/npm-cli install at /Users/ryjohn/.cache/firebase/tools/node_modules/npm/bin/npm-cli +Checking for npm/bin/npm-cli install at /snapshot/firepit/node_modules/npm/bin/npm-cli +Found npm/bin/npm-cli install. +ShellJSInternalError: ENOENT: no such file or directory, chmod '/Users/ryjohn/.cache/firebase/runtime/shell.js' \ No newline at end of file diff --git a/compass_25/ios/.gitignore b/compass_25/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/compass_25/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +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 diff --git a/compass_25/ios/Flutter/AppFrameworkInfo.plist b/compass_25/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/compass_25/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/compass_25/ios/Flutter/Debug.xcconfig b/compass_25/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/compass_25/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/compass_25/ios/Flutter/Release.xcconfig b/compass_25/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/compass_25/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/compass_25/ios/Runner.xcodeproj/project.pbxproj b/compass_25/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e7f978c --- /dev/null +++ b/compass_25/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,659 @@ +// !$*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 */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 62504F2E26E962AD4FA56B8D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 837CBC20DBFE8CF56BEB95DF /* GoogleService-Info.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 837CBC20DBFE8CF56BEB95DF /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 837CBC20DBFE8CF56BEB95DF /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + 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 = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + 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; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + 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 */, + 62504F2E26E962AD4FA56B8D /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 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"; + }; +/* 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 */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 36QNC2BPVK; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.cupertinoCompass; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.cupertinoCompass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.cupertinoCompass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.cupertinoCompass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + 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; + "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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 36QNC2BPVK; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.cupertinoCompass; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 36QNC2BPVK; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.cupertinoCompass; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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 */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/compass_25/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/compass_25/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/compass_25/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/compass_25/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/compass_25/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/compass_25/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/compass_25/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/compass_25/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/compass_25/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/compass_25/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/compass_25/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4a66255 --- /dev/null +++ b/compass_25/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,132 @@ +{ + "originHash" : "fb89d1fd429d53cf3105bb9a12ed9c2f8b2569bbf7266efe4d481ac8d66c3260", + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "d1f7c7e8eaa74d7e44467184dc5f592268247d33", + "version" : "11.11.0" + } + }, + { + "identity" : "flutterfire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/flutterfire", + "state" : { + "revision" : "a80a123386fd4904cad6938673020a7bcf31b2f2", + "version" : "3.13.0-firebase-core-swift" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "dd89fc79a77183830742a16866d87e4e54785734", + "version" : "11.11.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", + "version" : "1.69.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35", + "version" : "4.4.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", + "version" : "1.29.0" + } + } + ], + "version" : 3 +} diff --git a/compass_25/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/compass_25/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/compass_25/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compass_25/ios/Runner.xcworkspace/contents.xcworkspacedata b/compass_25/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/compass_25/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/compass_25/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/compass_25/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/compass_25/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/compass_25/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/compass_25/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/compass_25/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/compass_25/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/compass_25/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..7093c42 --- /dev/null +++ b/compass_25/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,132 @@ +{ + "originHash" : "fb89d1fd429d53cf3105bb9a12ed9c2f8b2569bbf7266efe4d481ac8d66c3260", + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4", + "version" : "11.10.0" + } + }, + { + "identity" : "flutterfire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/flutterfire", + "state" : { + "revision" : "a80a123386fd4904cad6938673020a7bcf31b2f2", + "version" : "3.13.0-firebase-core-swift" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6", + "version" : "11.10.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", + "version" : "1.69.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35", + "version" : "4.4.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", + "version" : "1.29.0" + } + } + ], + "version" : 3 +} diff --git a/compass_25/ios/Runner/AppDelegate.swift b/compass_25/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/compass_25/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..e6cbcd5 --- /dev/null +++ b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Splash.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Splash@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Splash@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Splash.png b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Splash.png new file mode 100644 index 0000000..541bff8 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Splash.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Splash@2x.png b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Splash@2x.png new file mode 100644 index 0000000..9804d9a Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Splash@2x.png differ diff --git a/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Splash@3x.png b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Splash@3x.png new file mode 100644 index 0000000..566e039 Binary files /dev/null and b/compass_25/ios/Runner/Assets.xcassets/LaunchImage.imageset/Splash@3x.png differ diff --git a/compass_25/ios/Runner/Base.lproj/LaunchScreen.storyboard b/compass_25/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..bcf1680 --- /dev/null +++ b/compass_25/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compass_25/ios/Runner/Base.lproj/Main.storyboard b/compass_25/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/compass_25/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compass_25/ios/Runner/GoogleService-Info.plist b/compass_25/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..9fb5eda --- /dev/null +++ b/compass_25/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAYYizz-a19MKGTS_hrZtOcQPrU7PpzlwQ + GCM_SENDER_ID + 1025902230344 + PLIST_VERSION + 1 + BUNDLE_ID + com.example.cupertinoCompass + PROJECT_ID + compass-ios-455022 + STORAGE_BUCKET + compass-ios-455022.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1025902230344:ios:a0ce366519e0fc27b6117d + + \ No newline at end of file diff --git a/compass_25/ios/Runner/Info.plist b/compass_25/ios/Runner/Info.plist new file mode 100644 index 0000000..bf7acfd --- /dev/null +++ b/compass_25/ios/Runner/Info.plist @@ -0,0 +1,76 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Cupertino Compass + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + cupertino_compass + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSLocationWhenInUseUsageDescription + This app needs access to location when open. + NSLocationAlwaysUsageDescription + This app needs access to location when in the background. + NSLocationAlwaysAndWhenInUseUsageDescription + This app needs access to location when open and in the background. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + + + diff --git a/compass_25/ios/Runner/Runner-Bridging-Header.h b/compass_25/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/compass_25/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/compass_25/ios/RunnerTests/RunnerTests.swift b/compass_25/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/compass_25/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/compass_25/lib/adaptive/bottom_sheet.dart b/compass_25/lib/adaptive/bottom_sheet.dart new file mode 100644 index 0000000..d8c0065 --- /dev/null +++ b/compass_25/lib/adaptive/bottom_sheet.dart @@ -0,0 +1,47 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; + +void showBottomSheetAdaptive({ + required BuildContext context, + required Widget child, + NavigatorState? navigatorState, +}) { + navigatorState ??= Navigator.of(context); + + if (defaultTargetPlatform == TargetPlatform.iOS) { + navigatorState.push(cupertinoRoute(child)); + } else { + navigatorState.push(androidRoute(child)); + } +} + +PageRoute cupertinoRoute(Widget child) { + return CupertinoSheetRoute(builder: (context) => child); +} + +PageRoute androidRoute(Widget child) { + return PageRouteBuilder( + opaque: false, + pageBuilder: + ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => child, + transitionsBuilder: + ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) => SlideTransition( + position: Tween( + begin: const Offset(1.0, 0.0), + end: Offset.zero, + ).animate( + CurvedAnimation(parent: animation, curve: Curves.easeInOut), + ), + child: child, + ), + ); +} diff --git a/compass_25/lib/adaptive/button.dart b/compass_25/lib/adaptive/button.dart new file mode 100644 index 0000000..c4cefd1 --- /dev/null +++ b/compass_25/lib/adaptive/button.dart @@ -0,0 +1,68 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class Button extends StatelessWidget { + final Widget child; + final VoidCallback? onPressed; + final Color? color; + final BorderRadius? borderRadius; + final bool small; + + const Button({ + super.key, + required this.onPressed, + required this.child, + this.color, + this.borderRadius, + this.small = false, // Added the small parameter with a default value + }); + + @override + Widget build(BuildContext context) { + final buttonPadding = + small + ? const EdgeInsets.symmetric(horizontal: 16.0) + : const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0); + final buttonTextStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: small ? Theme.of(context).textTheme.bodyMedium?.fontSize : null, + ); + + final fullyRoundedRadius = 24.0; // A sufficiently large radius + + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + return CupertinoButton( + onPressed: onPressed, + color: color, + borderRadius: BorderRadius.circular(fullyRoundedRadius), + padding: EdgeInsets.zero, + child: Padding( + padding: buttonPadding, + child: DefaultTextStyle( + style: buttonTextStyle ?? const TextStyle(), + child: child, + ), + ), + ); + } else { + return MaterialButton( + onPressed: onPressed, + color: color, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(fullyRoundedRadius), + ), + padding: buttonPadding, + child: DefaultTextStyle( + style: buttonTextStyle ?? const TextStyle(), + child: child, + ), + ); + } + } +} \ No newline at end of file diff --git a/compass_25/lib/adaptive/loading.dart b/compass_25/lib/adaptive/loading.dart new file mode 100644 index 0000000..e98ce32 --- /dev/null +++ b/compass_25/lib/adaptive/loading.dart @@ -0,0 +1,25 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'dart:io' show Platform; + +/// A widget that switches between CircularProgressIndicator on Android, +/// and CupertinoActivityIndicator on iOS. +class AdaptiveLoadingIndicator extends StatelessWidget { + final double? radius; + final Color? color; + + const AdaptiveLoadingIndicator({super.key, this.radius, this.color}); + + @override + Widget build(BuildContext context) { + if (Platform.isIOS) { + return CupertinoActivityIndicator(radius: radius ?? 10); + } else { + return CircularProgressIndicator(color: color); + } + } +} diff --git a/compass_25/lib/adaptive/switch.dart b/compass_25/lib/adaptive/switch.dart new file mode 100644 index 0000000..e75ba49 --- /dev/null +++ b/compass_25/lib/adaptive/switch.dart @@ -0,0 +1,34 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart' as cupertino; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' as material; +import 'package:flutter/widgets.dart'; + +class Switch extends StatelessWidget { + const Switch({ + super.key, + required this.value, + required this.onChanged, + this.activeColor, + this.activeTrackColor, + }); + + final bool value; + final ValueChanged? onChanged; + final Color? activeColor; + final Color? activeTrackColor; + + @override + Widget build(BuildContext context) { + return switch (defaultTargetPlatform) { + TargetPlatform.iOS || TargetPlatform.macOS => cupertino.CupertinoSwitch( + value: value, + onChanged: onChanged, + ), + _ => material.Switch(value: value, onChanged: onChanged), + }; + } +} diff --git a/compass_25/lib/adaptive/text_input.dart b/compass_25/lib/adaptive/text_input.dart new file mode 100644 index 0000000..ca5bfe2 --- /dev/null +++ b/compass_25/lib/adaptive/text_input.dart @@ -0,0 +1,143 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart' as cupertino; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' as material; +import 'package:flutter/material.dart'; + +class AdaptiveTextField extends Widget { + final TextEditingController? controller; + final String? placeholder; + final TextInputType? keyboardType; + final bool obscureText; + final String? Function(String?)? validator; + final void Function(String)? onChanged; + final void Function(String)? onSubmitted; + final FocusNode? focusNode; + final TextInputAction? textInputAction; + final int? maxLines; + final int? minLines; + final int? maxLength; + final bool expands; + final TextCapitalization textCapitalization; + final TextStyle? style; + final InputDecoration? decoration; + final BoxDecoration? cupertinoDecoration; + final EdgeInsetsGeometry? padding; + final TextAlign textAlign; + final TextAlignVertical? textAlignVertical; + final bool readOnly; + final bool autofocus; + final Color? cursorColor; + final Radius? cursorRadius; + final double cursorWidth; + + const AdaptiveTextField({ + super.key, + this.controller, + this.placeholder, + this.keyboardType, + this.obscureText = false, + this.validator, + this.onChanged, + this.onSubmitted, + this.focusNode, + this.textInputAction, + this.maxLines = 1, + this.minLines, + this.maxLength, + this.expands = false, + this.textCapitalization = TextCapitalization.none, + this.style, + this.decoration, + this.cupertinoDecoration, + this.padding, + this.textAlign = TextAlign.start, + this.textAlignVertical, + this.readOnly = false, + this.autofocus = false, + this.cursorColor, + this.cursorRadius, + this.cursorWidth = 2.0, + }); + + @override + Element createElement() { + return switch (defaultTargetPlatform) { + TargetPlatform.iOS || + TargetPlatform.macOS => CupertinoTextFieldElement(this), + _ => MaterialTextFieldElement(this), + }; + } +} + +class MaterialTextFieldElement extends ComponentElement { + MaterialTextFieldElement(AdaptiveTextField super.widget); + + @override + Widget build() { + final adaptiveWidget = widget as AdaptiveTextField; + return material.TextFormField( + controller: adaptiveWidget.controller, + decoration: + adaptiveWidget.decoration ?? + material.InputDecoration(hintText: adaptiveWidget.placeholder), + keyboardType: adaptiveWidget.keyboardType, + obscureText: adaptiveWidget.obscureText, + validator: adaptiveWidget.validator, + onChanged: adaptiveWidget.onChanged, + onFieldSubmitted: adaptiveWidget.onSubmitted, + focusNode: adaptiveWidget.focusNode, + textInputAction: adaptiveWidget.textInputAction, + maxLines: adaptiveWidget.maxLines, + minLines: adaptiveWidget.minLines, + maxLength: adaptiveWidget.maxLength, + expands: adaptiveWidget.expands, + textCapitalization: adaptiveWidget.textCapitalization, + style: adaptiveWidget.style, + textAlign: adaptiveWidget.textAlign, + textAlignVertical: adaptiveWidget.textAlignVertical, + readOnly: adaptiveWidget.readOnly, + autofocus: adaptiveWidget.autofocus, + cursorColor: adaptiveWidget.cursorColor, + cursorRadius: adaptiveWidget.cursorRadius, + cursorWidth: adaptiveWidget.cursorWidth, + ); + } +} + +class CupertinoTextFieldElement extends ComponentElement { + CupertinoTextFieldElement(AdaptiveTextField super.widget); + + @override + Widget build() { + final adaptiveWidget = widget as AdaptiveTextField; + return cupertino.CupertinoTextField( + controller: adaptiveWidget.controller, + placeholder: adaptiveWidget.placeholder, + keyboardType: adaptiveWidget.keyboardType, + obscureText: adaptiveWidget.obscureText, + onChanged: adaptiveWidget.onChanged, + onSubmitted: adaptiveWidget.onSubmitted, + focusNode: adaptiveWidget.focusNode, + textInputAction: adaptiveWidget.textInputAction, + maxLines: adaptiveWidget.maxLines, + minLines: adaptiveWidget.minLines, + maxLength: adaptiveWidget.maxLength, + expands: adaptiveWidget.expands, + textCapitalization: adaptiveWidget.textCapitalization, + style: adaptiveWidget.style, + decoration: adaptiveWidget.cupertinoDecoration, + padding: adaptiveWidget.padding ?? EdgeInsets.all(7.0), + textAlign: adaptiveWidget.textAlign, + textAlignVertical: adaptiveWidget.textAlignVertical, + readOnly: adaptiveWidget.readOnly, + autofocus: adaptiveWidget.autofocus, + cursorColor: adaptiveWidget.cursorColor, + cursorRadius: adaptiveWidget.cursorRadius ?? const Radius.circular(2.0), + cursorWidth: adaptiveWidget.cursorWidth, + ); + } +} diff --git a/compass_25/lib/adaptive/widgets.dart b/compass_25/lib/adaptive/widgets.dart new file mode 100644 index 0000000..8129d7f --- /dev/null +++ b/compass_25/lib/adaptive/widgets.dart @@ -0,0 +1,11 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'package:flutter/widgets.dart' hide Icon; + +export 'bottom_sheet.dart'; +export 'button.dart'; +export 'loading.dart'; +export 'switch.dart'; +export 'text_input.dart'; diff --git a/compass_25/lib/auth.dart b/compass_25/lib/auth.dart new file mode 100644 index 0000000..355331d --- /dev/null +++ b/compass_25/lib/auth.dart @@ -0,0 +1,25 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; + +class CompassAppAuth extends ValueNotifier { + CompassAppAuth() : super(FirebaseAuth.instance.currentUser) { + FirebaseAuth.instance.authStateChanges().listen((User? user) { + value = user; + }); + } + + String? get userId => value?.uid; + bool get isLoggedIn => value != null; + + Future login() async { + await FirebaseAuth.instance.signInAnonymously(); + } + + Future logout() async { + await FirebaseAuth.instance.signOut(); + } +} diff --git a/compass_25/lib/firebase_options.dart b/compass_25/lib/firebase_options.dart new file mode 100644 index 0000000..4a88712 --- /dev/null +++ b/compass_25/lib/firebase_options.dart @@ -0,0 +1,74 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyDeslzXMJOcRW77BV-5XYTJWJ0D-00hCq8', + appId: '1:1025902230344:web:eb9ca50d538d3419b6117d', + messagingSenderId: '1025902230344', + projectId: 'compass-ios-455022', + authDomain: 'compass-ios-455022.firebaseapp.com', + storageBucket: 'compass-ios-455022.firebasestorage.app', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyD5s-frlzmlN8xRuClgdZPJ00HKZVgBd7A', + appId: '1:1025902230344:android:5623a28d48302226b6117d', + messagingSenderId: '1025902230344', + projectId: 'compass-ios-455022', + storageBucket: 'compass-ios-455022.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAYYizz-a19MKGTS_hrZtOcQPrU7PpzlwQ', + appId: '1:1025902230344:ios:a0ce366519e0fc27b6117d', + messagingSenderId: '1025902230344', + projectId: 'compass-ios-455022', + storageBucket: 'compass-ios-455022.firebasestorage.app', + iosBundleId: 'com.example.cupertinoCompass', + ); +} diff --git a/compass_25/lib/main.dart b/compass_25/lib/main.dart new file mode 100644 index 0000000..e352536 --- /dev/null +++ b/compass_25/lib/main.dart @@ -0,0 +1,60 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'auth.dart'; +import 'firebase_options.dart'; +import 'model/app_data.dart'; +import 'router.dart'; +import 'theme.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + runApp(const CompassApp()); +} + +class CompassApp extends StatefulWidget { + const CompassApp({super.key}); + + @override + State createState() => _CompassAppState(); +} + +class _CompassAppState extends State { + late final CompassAppData _appData; + late final CompassAppAuth _auth; + late final CompassAppRouter _appRouter; + + @override + void initState() { + super.initState(); + _auth = CompassAppAuth(); + _appData = CompassAppData(auth: _auth); + _appRouter = CompassAppRouter(_auth); + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + Provider.value(value: _appData), + ChangeNotifierProvider.value(value: _auth), + ], + child: MaterialApp.router( + title: 'Compass', + routerConfig: _appRouter.router, + debugShowCheckedModeBanner: false, + theme: lightTheme, + darkTheme: darkTheme, + themeMode: ThemeMode.light, + ), + ); + } +} diff --git a/compass_25/lib/model/activity.dart b/compass_25/lib/model/activity.dart new file mode 100644 index 0000000..578511e --- /dev/null +++ b/compass_25/lib/model/activity.dart @@ -0,0 +1,57 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:json_annotation/json_annotation.dart'; + +part 'activity.g.dart'; + +enum TimeOfDay { + any, + morning, + afternoon, + evening, + night; + + @override + String toString() { + return super.toString().split('.').last; + } +} + +@JsonSerializable() +class Activity { + final String name; + final String description; + final String locationName; + final int duration; + final TimeOfDay timeOfDay; + final bool familyFriendly; + final int price; + final String destinationRef; + final String ref; + final String imageUrl; + + Activity({ + required this.name, + required this.description, + required this.locationName, + required this.duration, + required this.timeOfDay, + required this.familyFriendly, + required this.price, + required this.destinationRef, + required this.ref, + required this.imageUrl, + }); + + factory Activity.fromJson(Map json) => + _$ActivityFromJson(json); + + Map toJson() => _$ActivityToJson(this); + + @override + String toString() { + return 'Activity{name: $name}'; + } +} diff --git a/compass_25/lib/model/activity.g.dart b/compass_25/lib/model/activity.g.dart new file mode 100644 index 0000000..c166f36 --- /dev/null +++ b/compass_25/lib/model/activity.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'activity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Activity _$ActivityFromJson(Map json) => Activity( + name: json['name'] as String, + description: json['description'] as String, + locationName: json['locationName'] as String, + duration: (json['duration'] as num).toInt(), + timeOfDay: $enumDecode(_$TimeOfDayEnumMap, json['timeOfDay']), + familyFriendly: json['familyFriendly'] as bool, + price: (json['price'] as num).toInt(), + destinationRef: json['destinationRef'] as String, + ref: json['ref'] as String, + imageUrl: json['imageUrl'] as String, +); + +Map _$ActivityToJson(Activity instance) => { + 'name': instance.name, + 'description': instance.description, + 'locationName': instance.locationName, + 'duration': instance.duration, + 'timeOfDay': _$TimeOfDayEnumMap[instance.timeOfDay]!, + 'familyFriendly': instance.familyFriendly, + 'price': instance.price, + 'destinationRef': instance.destinationRef, + 'ref': instance.ref, + 'imageUrl': instance.imageUrl, +}; + +const _$TimeOfDayEnumMap = { + TimeOfDay.any: 'any', + TimeOfDay.morning: 'morning', + TimeOfDay.afternoon: 'afternoon', + TimeOfDay.evening: 'evening', + TimeOfDay.night: 'night', +}; diff --git a/compass_25/lib/model/app_data.dart b/compass_25/lib/model/app_data.dart new file mode 100644 index 0000000..a58b718 --- /dev/null +++ b/compass_25/lib/model/app_data.dart @@ -0,0 +1,153 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:cupertino_compass/auth.dart'; +import 'package:flutter/services.dart'; + +import 'activity.dart'; +import 'booking.dart'; +import 'continent.dart'; +import 'destination.dart'; + +class CompassAppData { + final CompassAppAuth _auth; + List? _destinations; + List? _activities; + + final _bookingsController = StreamController>.broadcast(); + StreamSubscription? _bookingsSubscription; + + CompassAppData({required CompassAppAuth auth}) : _auth = auth { + // Listen for auth state changes, and subscribe to bookings + // if the user is logged in. + _auth.addListener(() { + if (_auth.isLoggedIn) { + _listenForBookingChanges(); + } else { + _bookingsSubscription?.cancel(); + } + }); + getDestinations(); + } + + Stream> get bookingStream => _bookingsController.stream; + + CollectionReference> _bookingsRef(String? userId) { + if (userId == null) { + throw Exception('Null User ID while accessing Firestore'); + } + return FirebaseFirestore.instance + .collection('users') + .doc(userId) + .collection('bookings'); + } + + Future saveBooking(Booking booking) async { + final bookingsCollection = _bookingsRef(_auth.userId); + await bookingsCollection.add(booking.toJson()); + } + + Future> getBookings() async { + final bookingsCollection = _bookingsRef(_auth.userId); + final snapshot = await bookingsCollection.get(); + return snapshot.docs.map((doc) => Booking.fromJson(doc.data())).toList(); + } + + void _listenForBookingChanges() { + final bookingsCollection = _bookingsRef(_auth.userId); + _bookingsSubscription = bookingsCollection.snapshots().listen((snapshot) { + final bookings = + snapshot.docs.map((doc) => Booking.fromJson(doc.data())).toList(); + _bookingsController.add(bookings); + }); + } + + Future> getDestinations() async { + if (_destinations == null) { + final destinationData = await _loadStringAsset( + 'assets/destinations.json', + ); + _destinations = + destinationData.map((e) => Destination.fromJson(e)).toList(); + } + return _destinations!; + } + + Destination getDestination(String ref) { + if (_destinations == null) { + throw (StateError('Destinations not initialized')); + } + return _destinations!.firstWhere((d) => d.ref == ref); + } + + Future> getActivities(String destinationRef) async { + if (_activities == null) { + final activityData = await _loadStringAsset('assets/activities.json'); + _activities = activityData.map((e) => Activity.fromJson(e)).toList(); + } + + return _activities! + .where((a) => a.destinationRef == destinationRef) + .toList(); + } + + List getActivitiesFromRefs(List refs) { + if (_activities == null) throw (StateError('Activities not initialized')); + return _activities!.where((a) => refs.contains(a.ref)).toList(); + } + + List get continents { + return const [ + Continent( + name: 'Europe', + imageUrl: 'https://rstr.in/google/tripedia/TmR12QdlVTT', + location: Location(54.5260, 15.2551), + ), + Continent( + name: 'Asia', + imageUrl: 'https://rstr.in/google/tripedia/VJ8BXlQg8O1', + location: Location(34.0479, 100.6197), + ), + Continent( + name: 'South America', + imageUrl: 'https://rstr.in/google/tripedia/flm_-o1aI8e', + location: Location(-8.7832, -55.4915), + ), + Continent( + name: 'Africa', + imageUrl: 'https://rstr.in/google/tripedia/-nzi8yFOBpF', + location: Location(7.1881, 21.0936), + ), + Continent( + name: 'North America', + imageUrl: 'https://rstr.in/google/tripedia/jlbgFDrSUVE', + location: Location(37.0902, -95.7129), + ), + Continent( + name: 'Oceania', + imageUrl: 'https://rstr.in/google/tripedia/vxyrDE-fZVL', + location: Location(-22.7832, 140.4915), + ), + Continent( + name: 'Australia', + imageUrl: 'https://rstr.in/google/tripedia/z6vy6HeRyvZ', + location: Location(-25.2744, 133.7751), + ), + ]; + } + + void dispose() { + _bookingsController.close(); + _bookingsSubscription?.cancel(); + } +} + +Future> _loadStringAsset(String asset) async { + final localData = await rootBundle.loadString(asset); + return jsonDecode(localData); +} diff --git a/compass_25/lib/model/booking.dart b/compass_25/lib/model/booking.dart new file mode 100644 index 0000000..a1bef07 --- /dev/null +++ b/compass_25/lib/model/booking.dart @@ -0,0 +1,27 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:json_annotation/json_annotation.dart'; + +part 'booking.g.dart'; + +@JsonSerializable() +class Booking { + final DateTime startDate; + final DateTime endDate; + final String destinationRef; + final List activitiesRefs; + + Booking({ + required this.startDate, + required this.endDate, + required this.destinationRef, + required this.activitiesRefs, + }); + + factory Booking.fromJson(Map json) => + _$BookingFromJson(json); + + Map toJson() => _$BookingToJson(this); +} diff --git a/compass_25/lib/model/booking.g.dart b/compass_25/lib/model/booking.g.dart new file mode 100644 index 0000000..8b3e6ad --- /dev/null +++ b/compass_25/lib/model/booking.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'booking.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Booking _$BookingFromJson(Map json) => Booking( + startDate: DateTime.parse(json['startDate'] as String), + endDate: DateTime.parse(json['endDate'] as String), + destinationRef: json['destinationRef'] as String, + activitiesRefs: + (json['activitiesRefs'] as List) + .map((e) => e as String) + .toList(), +); + +Map _$BookingToJson(Booking instance) => { + 'startDate': instance.startDate.toIso8601String(), + 'endDate': instance.endDate.toIso8601String(), + 'destinationRef': instance.destinationRef, + 'activitiesRefs': instance.activitiesRefs, +}; diff --git a/compass_25/lib/model/continent.dart b/compass_25/lib/model/continent.dart new file mode 100644 index 0000000..5aaaf88 --- /dev/null +++ b/compass_25/lib/model/continent.dart @@ -0,0 +1,27 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:json_annotation/json_annotation.dart'; + +import 'destination.dart'; + +part 'continent.g.dart'; + +@JsonSerializable() +class Continent { + final String name; + final String imageUrl; + final Location location; + + const Continent({ + required this.name, + required this.imageUrl, + required this.location, + }); + + factory Continent.fromJson(Map json) => + _$ContinentFromJson(json); + + Map toJson() => _$ContinentToJson(this); +} diff --git a/compass_25/lib/model/continent.g.dart b/compass_25/lib/model/continent.g.dart new file mode 100644 index 0000000..212075b --- /dev/null +++ b/compass_25/lib/model/continent.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'continent.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Continent _$ContinentFromJson(Map json) => Continent( + name: json['name'] as String, + imageUrl: json['imageUrl'] as String, + location: Location.fromJson(json['location'] as Map), +); + +Map _$ContinentToJson(Continent instance) => { + 'name': instance.name, + 'imageUrl': instance.imageUrl, + 'location': instance.location, +}; diff --git a/compass_25/lib/model/destination.dart b/compass_25/lib/model/destination.dart new file mode 100644 index 0000000..d3713e6 --- /dev/null +++ b/compass_25/lib/model/destination.dart @@ -0,0 +1,63 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:json_annotation/json_annotation.dart'; + +part 'destination.g.dart'; + +@JsonSerializable() +class Destination { + /// e.g. 'alaska' + final String ref; + + /// e.g. 'Alaska' + final String name; + + /// e.g. 'United States' + final String country; + + /// e.g. 'North America' + final String continent; + + /// e.g. 'Alaska is a haven for outdoor enthusiasts ...' + final String knownFor; + + /// e.g. 'Alaska is a haven for outdoor enthusiasts ...' + final List tags; + + /// e.g. 'https://storage.googleapis.com/tripedia-images/destinations/alaska.jpg' + final String imageUrl; + + /// e.g. {"latitude": 64.2008, "longitude": -149.4937} + final Location location; + + Destination( + this.ref, + this.name, + this.country, + this.continent, + this.knownFor, + this.tags, + this.imageUrl, + this.location, + ); + + factory Destination.fromJson(Map json) => + _$DestinationFromJson(json); + + Map toJson() => _$DestinationToJson(this); +} + +@JsonSerializable() +class Location { + final double latitude; + final double longitude; + + const Location(this.latitude, this.longitude); + + factory Location.fromJson(Map json) => + _$LocationFromJson(json); + + Map toJson() => _$LocationToJson(this); +} diff --git a/compass_25/lib/model/destination.g.dart b/compass_25/lib/model/destination.g.dart new file mode 100644 index 0000000..0529586 --- /dev/null +++ b/compass_25/lib/model/destination.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'destination.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Destination _$DestinationFromJson(Map json) => Destination( + json['ref'] as String, + json['name'] as String, + json['country'] as String, + json['continent'] as String, + json['knownFor'] as String, + (json['tags'] as List).map((e) => e as String).toList(), + json['imageUrl'] as String, + Location.fromJson(json['location'] as Map), +); + +Map _$DestinationToJson(Destination instance) => + { + 'ref': instance.ref, + 'name': instance.name, + 'country': instance.country, + 'continent': instance.continent, + 'knownFor': instance.knownFor, + 'tags': instance.tags, + 'imageUrl': instance.imageUrl, + 'location': instance.location, + }; + +Location _$LocationFromJson(Map json) => Location( + (json['latitude'] as num).toDouble(), + (json['longitude'] as num).toDouble(), +); + +Map _$LocationToJson(Location instance) => { + 'latitude': instance.latitude, + 'longitude': instance.longitude, +}; diff --git a/compass_25/lib/model/model.dart b/compass_25/lib/model/model.dart new file mode 100644 index 0000000..65f7f9c --- /dev/null +++ b/compass_25/lib/model/model.dart @@ -0,0 +1,9 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'activity.dart'; +export 'app_data.dart'; +export 'booking.dart'; +export 'continent.dart'; +export 'destination.dart'; diff --git a/compass_25/lib/router.dart b/compass_25/lib/router.dart new file mode 100644 index 0000000..33bd69d --- /dev/null +++ b/compass_25/lib/router.dart @@ -0,0 +1,228 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:animations/animations.dart'; +import 'package:collection/collection.dart'; +import 'package:cupertino_compass/screens/explore.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'auth.dart'; +import 'model/app_data.dart'; +import 'scaffold.dart'; +import 'screens/login.dart'; +import 'screens/messages.dart'; +import 'screens/my_trips.dart'; +import 'screens/profile.dart'; + +final navigatorKey = GlobalKey(); +final myTripsNavKey = GlobalKey(debugLabel: 'my-trips'); +final exploreNavKey = GlobalKey(debugLabel: 'explore'); +final messagesNavKey = GlobalKey(debugLabel: 'messages'); +final profileNavKey = GlobalKey(debugLabel: 'profile'); + +class CompassAppRouter { + final CompassAppAuth auth; + + CompassAppRouter(this.auth); + + late final GoRouter router = GoRouter( + initialLocation: '/explore', + navigatorKey: navigatorKey, + refreshListenable: auth, + redirect: (BuildContext context, GoRouterState state) { + final loggedIn = auth.isLoggedIn; + final loggingIn = state.matchedLocation == '/login'; + + if (!loggedIn) { + return loggingIn ? null : '/login'; + } + + if (loggingIn) { + return '/explore'; + } + + return null; + }, + routes: [ + GoRoute(path: '/login', builder: (context, state) => LoginScreen()), + StatefulShellRoute( + parentNavigatorKey: navigatorKey, + builder: ( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigationShell, + ) { + return ScaffoldWithNavBar(navigationShell: navigationShell); + }, + navigatorContainerBuilder: ( + BuildContext context, + StatefulNavigationShell navigationShell, + List children, + ) { + return _AnimatedBranchContainer( + navigationShell: navigationShell, + children: children, + ); + }, + branches: [ + StatefulShellBranch( + navigatorKey: exploreNavKey, + routes: [ + GoRoute( + path: '/explore', + builder: (context, state) => ExploreScreen(), + ), + ], + ), + StatefulShellBranch( + navigatorKey: myTripsNavKey, + routes: [ + GoRoute( + path: '/my_trips', + builder: + (context, state) => + MyTripsScreen(appData: context.read()), + ), + ], + ), + StatefulShellBranch( + navigatorKey: messagesNavKey, + routes: [ + GoRoute( + path: '/messages', + builder: (context, state) => MessagesScreen(), + ), + ], + ), + StatefulShellBranch( + navigatorKey: profileNavKey, + routes: [ + GoRoute( + path: '/profile', + builder: (context, state) => ProfileScreen(), + ), + ], + ), + ], + ), + ], + ); +} + +class _AnimatedBranchContainer extends StatefulWidget { + const _AnimatedBranchContainer({ + required this.navigationShell, + required this.children, + }); + + final StatefulNavigationShell navigationShell; + final List children; + + @override + State<_AnimatedBranchContainer> createState() => + _AnimatedBranchContainerState(); +} + +class _AnimatedBranchContainerState extends State<_AnimatedBranchContainer> { + @override + Widget build(BuildContext context) { + return Stack( + children: + widget.children.mapIndexed((int index, Widget child) { + return _AnimatedOffstageNavigator( + isActive: widget.navigationShell.currentIndex == index, + child: child, + ); + }).toList(), + ); + } +} + +class _AnimatedOffstageNavigator extends StatefulWidget { + const _AnimatedOffstageNavigator({ + required this.isActive, + required this.child, + }); + + final bool isActive; + final Widget child; + + @override + State<_AnimatedOffstageNavigator> createState() => + _AnimatedOffstageNavigatorState(); +} + +class _AnimatedOffstageNavigatorState extends State<_AnimatedOffstageNavigator> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + late Animation _secondaryAnimation; + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + _animation = _controller.drive(CurveTween(curve: Curves.easeOutQuad)); + _secondaryAnimation = ReverseAnimation(_animation); + if (widget.isActive) { + _controller.forward(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant _AnimatedOffstageNavigator oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isActive != oldWidget.isActive) { + if (widget.isActive) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + } + + @override + Widget build(BuildContext context) { + final bool isIOS = Theme.of(context).platform == TargetPlatform.iOS; + return AnimatedBuilder( + animation: _animation, + builder: (BuildContext context, Widget? child) { + return Positioned.fill( + child: IgnorePointer( + ignoring: !widget.isActive, + child: + isIOS + ? FadeTransition( + opacity: _animation, + child: TickerMode( + enabled: widget.isActive, + child: child!, + ), + ) + : FadeThroughTransition( + animation: _animation, + fillColor: Colors.transparent, + secondaryAnimation: _secondaryAnimation, + child: TickerMode( + enabled: widget.isActive, + child: child!, + ), + ), + ), + ); + }, + child: widget.child, + ); + } +} diff --git a/compass_25/lib/scaffold.dart b/compass_25/lib/scaffold.dart new file mode 100644 index 0000000..1d10786 --- /dev/null +++ b/compass_25/lib/scaffold.dart @@ -0,0 +1,103 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'theme.dart'; + +class _Location { + String route; + String label; + String iconAsset; + + _Location(this.route, this.label, this.iconAsset); +} + +final List<_Location> _locations = [ + _Location('/explore', 'Explore', 'assets/icons/explore.png'), + _Location('/my_trips', 'My Activities', 'assets/icons/directions_run.png'), + _Location('/messages', 'Messages', 'assets/icons/chat.png'), + _Location('/profile', 'Profile', 'assets/icons/user.png'), +]; + +class ScaffoldWithNavBar extends StatelessWidget { + final StatefulNavigationShell navigationShell; + const ScaffoldWithNavBar({super.key, required this.navigationShell}); + @override + Widget build(BuildContext context) { + var currentIndex = _calculateSelectedIndex(context); + return Scaffold( + body: navigationShell, + bottomNavigationBar: + Platform.isIOS + ? CupertinoTabBar( + backgroundColor: Colors.white, + border: const Border( + top: BorderSide(color: Colors.grey, width: 0.0), + ), + iconSize: 24, + items: + _locations.mapIndexed((index, location) { + return BottomNavigationBarItem( + icon: Image.asset( + location.iconAsset, + width: 24, + height: 24, + color: + index == currentIndex + ? AppColors.primary + : Colors.grey, + ), + label: location.label, + ); + }).toList(), + currentIndex: currentIndex, + onTap: (int index) => _onItemTapped(index, context), + ) + : BottomNavigationBar( + //Removed SizedBox here. + type: BottomNavigationBarType.fixed, + items: + _locations + .mapIndexed( + (index, location) => BottomNavigationBarItem( + label: location.label, + icon: Image.asset( + location.iconAsset, + color: + index == currentIndex + ? AppColors.primary + : Colors.grey, + width: 24, + height: 24, + ), + ), + ) + .toList(), + currentIndex: _calculateSelectedIndex(context), + selectedItemColor: Theme.of(context).colorScheme.primary, + selectedIconTheme: IconThemeData(color: AppColors.primary), + unselectedFontSize: 10, + selectedFontSize: 12, + onTap: (int idx) => _onItemTapped(idx, context), + ), + ); + } + + static int _calculateSelectedIndex(BuildContext context) { + final String uriPath = GoRouterState.of(context).uri.path; + for (var i = 0; i < _locations.length; i++) { + final location = _locations[i]; + if (uriPath.startsWith(location.route)) { + return i; + } + } + return 0; + } + + void _onItemTapped(int index, BuildContext context) { + navigationShell.goBranch(index); + } +} diff --git a/compass_25/lib/screens/activities.dart b/compass_25/lib/screens/activities.dart new file mode 100644 index 0000000..1fd8ac5 --- /dev/null +++ b/compass_25/lib/screens/activities.dart @@ -0,0 +1,164 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cupertino_compass/adaptive/bottom_sheet.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../adaptive/loading.dart'; +import '../adaptive/widgets.dart' as adaptive; +import '../model/activity.dart'; +import '../model/app_data.dart'; +import '../model/booking.dart'; +import '../model/destination.dart'; +import '../theme.dart'; +import '../widgets/activity.dart'; +import 'booking.dart'; + +class ActivitiesScreen extends StatefulWidget { + final Destination destination; + final DateTimeRange? dateTimeRange; + const ActivitiesScreen({ + required this.destination, + this.dateTimeRange, + super.key, + }); + + @override + State createState() => _ActivitiesScreenState(); +} + +class _ActivitiesScreenState extends State { + List? _activities; + List _selected = []; + + @override + void initState() { + super.initState(); + _loadActivities(); + } + + List get _selectedActivities { + if (_activities == null) { + return []; + } + var result = []; + for (var i = 0; i < _activities!.length; i++) { + if (_selected[i] == true) { + result.add(_activities![i]); + } + } + return result; + } + + Future _loadActivities() async { + var result = await Provider.of( + context, + listen: false, + ).getActivities(widget.destination.ref); + + setState(() { + _activities = result; + _selected = List.filled(result.length, false); + }); + } + + @override + Widget build(BuildContext context) { + var activities = _activities; + + return Scaffold( + appBar: AppBar(title: Text('Activities')), + body: Column( + children: [ + if (activities != null) + Expanded( + child: ListView.builder( + itemBuilder: (BuildContext context, int idx) { + return ActivityEntry( + activity: activities[idx], + selected: _selected[idx], + showCheckbox: true, + onChanged: (bool? value) { + setState(() { + _selected[idx] = !_selected[idx]; + }); + }, + ); + }, + itemCount: activities.length, + ), + ) + else + Expanded(child: Center(child: AdaptiveLoadingIndicator())), + _BottomArea( + onPressed: () { + showBottomSheetAdaptive( + context: context, + child: BookingScreen( + booking: Booking( + destinationRef: widget.destination.ref, + startDate: widget.dateTimeRange?.start ?? DateTime.now(), + endDate: widget.dateTimeRange?.end ?? DateTime.now(), + activitiesRefs: + _selectedActivities.map((e) => e.ref).toList(), + ), + appData: context.read(), + ), + ); + }, + selectedActivities: _selectedActivities, + ), + ], + ), + ); + } +} + +class _BottomArea extends StatelessWidget { + final VoidCallback onPressed; + final List selectedActivities; + const _BottomArea({ + required this.onPressed, + required this.selectedActivities, + }); + + @override + Widget build(BuildContext context) { + return SafeArea( + bottom: true, + child: Container( + decoration: BoxDecoration( + border: Border(top: BorderSide(color: Colors.grey.shade200)), + ), + padding: EdgeInsets.only(left: 20, right: 20, top: 8, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "${selectedActivities.length} selected", + style: TextTheme.of( + context, + ).bodyLarge!.copyWith(color: Colors.grey.shade500), + ), + ), + adaptive.Button( + onPressed: selectedActivities.isNotEmpty ? onPressed : null, + color: AppColors.primary, + small: true, + child: Text( + 'Confirm', + style: Theme.of( + context, + ).textTheme.bodyMedium!.copyWith(color: Colors.white), + ), + ), + ], + ), + ), + ); + } +} diff --git a/compass_25/lib/screens/booking.dart b/compass_25/lib/screens/booking.dart new file mode 100644 index 0000000..3f8e798 --- /dev/null +++ b/compass_25/lib/screens/booking.dart @@ -0,0 +1,397 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'dart:ui'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../adaptive/widgets.dart' as adaptive; +import '../model/model.dart'; +import '../theme.dart'; +import '../utils.dart'; +import '../widgets/back_button.dart'; +import '../widgets/tag_chip.dart'; + +class BookingScreen extends StatefulWidget { + final CompassAppData appData; + final Booking booking; + final bool showBookingButton; + + const BookingScreen({ + required this.booking, + required this.appData, + this.showBookingButton = true, + super.key, + }); + + @override + State createState() => _BookingScreenState(); +} + +class _BookingScreenState extends State { + late final Destination destination; + List? activities; + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + + var appData = widget.appData; + + // Fetch the Destination for this booking + destination = appData.getDestination(widget.booking.destinationRef); + + // Fetch the activities associated with this booking + appData.getActivities(destination.ref).then((_) { + setState(() { + activities = appData.getActivitiesFromRefs( + widget.booking.activitiesRefs, + ); + }); + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + child: SizedBox( + height: 280, + child: Stack( + alignment: Alignment.topCenter, + children: [ + _HeaderImage(destination: destination), + const Positioned( + left: 0, + top: 0, + right: 0, + bottom: 0, + child: _Gradient(), + ), + Positioned( + bottom: 0, + left: 0, + child: _Headline( + booking: widget.booking, + destination: destination, + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 280), + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + destination.knownFor, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppColors.textGrey, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + top: 16.0, + ), + child: _Tags(destination: destination), + ), + Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + top: 30.0, + ), + child: Text( + 'Your Chosen Activities', + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontSize: 18), + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: activities?.length ?? 0, + itemBuilder: (context, index) { + final activity = activities![index]; + return Padding( + padding: const EdgeInsets.only( + top: 16.0, + left: 16.0, + right: 16.0, + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: activity.imageUrl, + height: 80, + width: 80, + fadeInDuration: Duration(milliseconds: 200), + fit: BoxFit.cover, + placeholder: + (context, url) => Container( + color: Colors.grey[300], + width: 80, + height: 80, + child: const Icon( + Icons.image_not_supported, + ), + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + activity.timeOfDay.toString().toUpperCase(), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.black54, + ), + ), + Text( + activity.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + Text( + activity.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + color: Colors.black54, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + const SizedBox(height: 120), + ], + ), + ), + ), + Positioned( + top: 24, + left: 24, + child: AdaptiveBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + if (widget.showBookingButton) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: _BookButton( + onPressed: () { + context.read().saveBooking(widget.booking); + + Navigator.of(context).popUntil((route) => route.isFirst); + context.go('/my_trips'); + }, + ), + ), + ], + ), + ); + } +} + +class _BookButton extends StatelessWidget { + final VoidCallback onPressed; + const _BookButton({required this.onPressed}); + + @override + Widget build(BuildContext context) { + final button = adaptive.Button( + onPressed: onPressed, + color: AppColors.primary, + borderRadius: BorderRadius.circular(10.0), + child: Text( + 'Book', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ); + return ClipRect( + child: + Platform.isIOS + ? BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + color: Theme.of( + context, + ).scaffoldBackgroundColor.withValues(alpha: 0.6), + padding: const EdgeInsets.only( + top: 16.0, + bottom: 48, + left: 16, + right: 16, + ), + child: SizedBox(width: double.infinity, child: button), + ), + ) + : Container( + color: Theme.of( + context, + ).scaffoldBackgroundColor.withValues(alpha: 0.6), + padding: const EdgeInsets.only( + top: 16.0, + bottom: 24, + left: 16, + right: 16, + ), + child: SizedBox(width: double.infinity, child: button), + ), + ); + } +} + +class _Tags extends StatelessWidget { + const _Tags({required this.destination}); + + final Destination destination; + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 6, + runSpacing: 6, + children: [ + ...destination.tags.map( + (tag) => TagChip( + tag: tag, + fontSize: 16, + height: 32, + chipColor: Colors.grey.shade200, + onChipColor: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ); + } +} + +class _Headline extends StatelessWidget { + const _Headline({required this.booking, required this.destination}); + + final Booking booking; + final Destination destination; + + @override + Widget build(BuildContext context) { + return Align( + alignment: AlignmentDirectional.bottomStart, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + destination.name, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 32, + ), + ), + Text( + dateFormatStartEnd( + DateTimeRange(start: booking.startDate, end: booking.endDate), + ), + style: Theme.of( + context, + ).textTheme.bodyLarge?.copyWith(color: AppColors.textGrey), + ), + ], + ), + ), + ); + } +} + +class _HeaderImage extends StatelessWidget { + const _HeaderImage({required this.destination}); + + final Destination destination; + + @override + Widget build(BuildContext context) { + return CachedNetworkImage( + imageUrl: destination.imageUrl, + fadeInDuration: Duration(milliseconds: 200), + fit: BoxFit.fitWidth, + width: MediaQuery.of(context).size.width, + placeholder: (context, url) { + return Container( + color: Colors.grey[300], + width: MediaQuery.of(context).size.width, + ); + }, + ); + } +} + +class _Gradient extends StatelessWidget { + const _Gradient(); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.3, 1.0], + colors: const [Colors.white10, Colors.white], + ), + ), + ); + } +} diff --git a/compass_25/lib/screens/explore.dart b/compass_25/lib/screens/explore.dart new file mode 100644 index 0000000..aeb447b --- /dev/null +++ b/compass_25/lib/screens/explore.dart @@ -0,0 +1,118 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cupertino_compass/adaptive/bottom_sheet.dart'; +import 'package:cupertino_compass/adaptive/loading.dart'; +import 'package:cupertino_compass/router.dart'; +import 'package:cupertino_compass/screens/activities.dart'; +import 'package:cupertino_compass/screens/search.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../model/app_data.dart'; +import '../model/destination.dart'; +import '../widgets/destination.dart'; + +class ExploreScreen extends StatefulWidget { + const ExploreScreen({super.key}); + + @override + State createState() => _ExploreScreenState(); +} + +class _ExploreScreenState extends State { + List? _destinations; + List? _visibleDestinations; + DateTimeRange? _dateTimeRange; + + @override + void initState() { + super.initState(); + _loadDestinations(); + } + + Future _loadDestinations() async { + var appData = Provider.of(context, listen: false); + var result = await appData.getDestinations(); + setState(() { + _destinations = result; + _visibleDestinations = _destinations?.toList(); + }); + } + + void _filterBySearch(Search search) { + setState(() { + if (search.continent == null) { + _visibleDestinations = _destinations; + } else { + _visibleDestinations = + _destinations + ?.where((d) => d.continent == search.continent?.name) + .toList(); + } + _dateTimeRange = search.dateTimeRange; + }); + } + + @override + Widget build(BuildContext context) { + var destinations = _visibleDestinations; + return Scaffold( + appBar: AppBar( + title: SizedBox(width: 128, child: Image.asset('assets/logo.png')), + actions: [ + IconButton( + icon: Icon(Icons.search), + onPressed: () { + showBottomSheetAdaptive( + navigatorState: navigatorKey.currentState, + context: context, + child: SearchScreen( + onSearch: (search) { + _filterBySearch(search); + }, + ), + ); + }, + ), + ], + ), + body: Column( + children: [ + if (destinations != null) + Expanded( + child: ListView.builder( + itemBuilder: (context, idx) { + return AspectRatio( + aspectRatio: 7 / 5, + child: Padding( + padding: EdgeInsets.all(8), + child: DestinationWidget( + destination: destinations[idx], + onTap: () { + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (BuildContext context) { + return ActivitiesScreen( + destination: destinations[idx], + dateTimeRange: _dateTimeRange, + ); + }, + ), + ); + }, + ), + ), + ); + }, + itemCount: destinations.length, + ), + ) + else + Expanded(child: Center(child: AdaptiveLoadingIndicator())), + ], + ), + ); + } +} diff --git a/compass_25/lib/screens/login.dart b/compass_25/lib/screens/login.dart new file mode 100644 index 0000000..c5986d5 --- /dev/null +++ b/compass_25/lib/screens/login.dart @@ -0,0 +1,98 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../auth.dart'; + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/login_bg.png"), + fit: BoxFit.cover, + ), + ), + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + Flexible( + flex: 2, + child: Image.asset( + 'assets/icon.png', + fit: BoxFit.scaleDown, + height: 64, + ), + ), + Spacer(flex: 1), + Flexible( + flex: 10, + child: Image.asset( + 'assets/logotype.png', + fit: BoxFit.scaleDown, + height: 64, + ), + ), + ], + ), + SizedBox(height: 60.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: ElevatedButton( + onPressed: () { + context.read().login(); + }, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.deepPurple.shade700, + backgroundColor: Colors.white, + minimumSize: Size(double.infinity, 50), + padding: EdgeInsets.symmetric(vertical: 15.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + ), + elevation: 5, + ), + child: Text('Sign in', style: TextStyle(fontSize: 16)), + ), + ), + SizedBox(height: 20.0), + TextButton( + onPressed: () { + // No-op + }, + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 20.0, + ), + ), + child: Text( + 'Sign up', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/compass_25/lib/screens/messages.dart b/compass_25/lib/screens/messages.dart new file mode 100644 index 0000000..ecc574f --- /dev/null +++ b/compass_25/lib/screens/messages.dart @@ -0,0 +1,196 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:cupertino_compass/widgets/title_text.dart'; +import 'package:flutter/material.dart'; + +import '../adaptive/widgets.dart' as adaptive; +import '../theme.dart'; +import '../widgets/chat_bubble.dart'; + +class MessagesScreen extends StatelessWidget { + const MessagesScreen({super.key}); + + @override + Widget build(BuildContext context) { + return ConversationListScreen(); + } +} + +class Conversation { + final String name; + final String latestMessage; + + Conversation({required this.name, required this.latestMessage}); +} + +class ConversationListScreen extends StatelessWidget { + const ConversationListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final conversations = [ + Conversation( + name: 'Alex Thompson', + latestMessage: 'Hey, how are you doing today?', + ), + Conversation( + name: 'Jordan Lee', + latestMessage: 'Just finished that report.', + ), + Conversation( + name: 'Casey Rivera', + latestMessage: 'See you at the meeting.', + ), + ]; + + return Scaffold( + appBar: AppBar(title: const TitleText(text: 'Messages')), + body: ListView.builder( + itemCount: conversations.length, + itemBuilder: (context, index) { + final conversation = conversations[index]; + + return ListTile( + leading: CircleAvatar( + foregroundColor: Colors.grey.shade500, + backgroundColor: Colors.grey.shade300, + child: Icon(Icons.person), + ), + title: Text( + conversation.name, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + subtitle: Text(conversation.latestMessage), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatScreen(name: conversation.name), + ), + ); + }, + ); + }, + ), + ); + } +} + +class ChatScreen extends StatefulWidget { + final String name; + const ChatScreen({super.key, required this.name}); + + @override + State createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + final List<({String? text, bool fromUser})> _messages = [ + (text: 'Hello!', fromUser: false), + (text: 'Hi there!', fromUser: true), + ]; + + final TextEditingController _textController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + + void _sendMessage(String message) { + if (message.isNotEmpty) { + setState(() { + _messages.add((text: message, fromUser: true)); + _textController.clear(); + _scrollDown(); + }); + } + } + + void _scrollDown() { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.name)), + body: Column( + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: _messages.length, + itemBuilder: (context, index) { + var isFromUser = _messages[index].fromUser; + return AdaptiveChatBubble( + message: _messages[index].text ?? "", + isUser: isFromUser, + iOSBackgroundColor: + isFromUser ? Colors.blue : Colors.grey.shade300, + iOSTextColor: isFromUser ? Colors.white : Colors.black, + androidBackgroundColor: + isFromUser + ? AppColors.chatBubbleUser + : AppColors.chatBubbleOther, + androidTextColor: Colors.black, + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.0), + border: Border.fromBorderSide( + BorderSide(color: Colors.grey.shade400, width: 1), + ), + ), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: adaptive.AdaptiveTextField( + padding: EdgeInsets.only(left: 16, top: 16, bottom: 16), + style: TextStyle(color: Colors.black), + controller: _textController, + placeholder: 'Type a message...', + onSubmitted: _sendMessage, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 0.0, + vertical: 0.0, + ), + hintText: 'Type a message...', + ), + ), + ), + ), + IconButton( + icon: Icon( + Platform.isIOS ? Icons.arrow_upward : Icons.send, + ), + onPressed: () => _sendMessage(_textController.text), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/compass_25/lib/screens/my_trips.dart b/compass_25/lib/screens/my_trips.dart new file mode 100644 index 0000000..a61577a --- /dev/null +++ b/compass_25/lib/screens/my_trips.dart @@ -0,0 +1,195 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:cupertino_compass/model/app_data.dart'; +import 'package:cupertino_compass/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../adaptive/widgets.dart' as adaptive; +import '../model/booking.dart'; +import '../router.dart'; +import '../widgets/title_text.dart'; +import 'booking.dart'; + +class MyTripsScreen extends StatefulWidget { + final CompassAppData appData; + const MyTripsScreen({required this.appData, super.key}); + + @override + State createState() => _MyTripsScreenState(); +} + +class _MyTripsScreenState extends State { + List? _bookings; + StreamSubscription>? _bookingsSubscription; + + @override + void initState() { + super.initState(); + widget.appData.getDestinations().then((_) { + _streamBookings(); + }); + } + + @override + void dispose() { + _bookingsSubscription?.cancel(); + super.dispose(); + } + + void _streamBookings() { + _bookingsSubscription = widget.appData.bookingStream.listen((newBookings) { + setState(() { + _bookings = newBookings; + }); + }); + _fetchInitialBookings(); + } + + Future _fetchInitialBookings() async { + var initialBookings = await widget.appData.getBookings(); + await widget.appData.getDestinations(); + setState(() { + _bookings = initialBookings; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: AppBar( + title: const TitleText(text: "Sofie's Activities", size: 32), + backgroundColor: Colors.white, + elevation: 0, + scrolledUnderElevation: 0, + ), + ), + if (_bookings == null) + SliverFixedExtentList.list( + itemExtent: 400, + children: [], + ) + else ...[ + SliverLayoutBuilder( + builder: (context, sliverConstraints) { + final viewPortSize = sliverConstraints.viewportMainAxisExtent; + final remainingPaintExtent = + viewPortSize - sliverConstraints.remainingPaintExtent; + double profileSize = 128.0; + if (remainingPaintExtent < 400) { + profileSize = remainingPaintExtent / 2; + if (profileSize < 96) { + profileSize = 96; + } + } + + return SliverToBoxAdapter( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: + Platform.isIOS + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: profileSize, + height: profileSize, + child: const _ProfilePicture(), + ), + ), + ], + ), + ); + }, + ), + if (_bookings!.isEmpty) + SliverFixedExtentList.list( + itemExtent: 64, + children: [ + Padding( + padding: EdgeInsets.all(16.0), + child: Align( + alignment: + Platform.isIOS + ? Alignment.center + : Alignment.centerLeft, + child: Text( + 'No activities yet', + style: TextTheme.of( + context, + ).bodyLarge?.copyWith(color: Colors.grey.shade600), + ), + ), + ), + ], + ) + else + SliverList( + delegate: SliverChildBuilderDelegate(( + BuildContext context, + int index, + ) { + var booking = _bookings![index]; + return ListTile( + title: Text( + widget.appData + .getDestination(booking.destinationRef) + .name, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + dateFormatStartEnd( + DateTimeRange( + start: booking.startDate, + end: booking.endDate, + ), + ), + ), + onTap: () { + adaptive.showBottomSheetAdaptive( + context: context, + navigatorState: navigatorKey.currentState, + child: BookingScreen( + booking: booking, + appData: context.read(), + showBookingButton: false, + ), + ); + }, + ); + }, childCount: _bookings!.length), + ), + ], + ], + ), + ); + } +} + +class _ProfilePicture extends StatelessWidget { + const _ProfilePicture(); + + @override + Widget build(BuildContext context) { + return ClipOval( + child: Image.asset( + "assets/user.jpg", + fit: BoxFit.cover, + semanticLabel: "Profile picture", + ), + ); + } +} diff --git a/compass_25/lib/screens/profile.dart b/compass_25/lib/screens/profile.dart new file mode 100644 index 0000000..fe9feef --- /dev/null +++ b/compass_25/lib/screens/profile.dart @@ -0,0 +1,299 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/link.dart'; + +import '../auth.dart'; +import '../theme.dart'; +import '../widgets/title_text.dart'; + +class ProfileScreen extends StatefulWidget { + const ProfileScreen({super.key}); + + @override + _ProfileScreenState createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + bool _notificationsEnabled = true; + final int _trips = 25; + final int _countriesVisited = 8; + DateTime? _selectedBirthdate; + + Future _selectDate(BuildContext context) async { + if (Platform.isIOS) { + showCupertinoModalPopup( + context: context, + builder: (BuildContext builder) { + return CupertinoTheme( + data: _defaultCupertinoThemeData, + child: Container( + height: MediaQuery.of(context).copyWith().size.height * 0.25, + color: Colors.white, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + onDateTimeChanged: (picked) { + setState(() { + _selectedBirthdate = picked; + }); + }, + initialDateTime: _selectedBirthdate ?? DateTime.now(), + minimumDate: DateTime(1900), + maximumDate: DateTime.now(), + ), + ), + ); + }, + ); + } else { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedBirthdate ?? DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime.now(), + ); + if (picked != null && picked != _selectedBirthdate) { + setState(() { + _selectedBirthdate = picked; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverLayoutBuilder( + builder: (context, sliverConstraints) { + final viewPortSize = sliverConstraints.viewportMainAxisExtent; + final remainingPaintExtent = + viewPortSize - sliverConstraints.remainingPaintExtent; + double profileSize = 200.0; + if (remainingPaintExtent < 200) { + profileSize = remainingPaintExtent; + if (profileSize < 128) { + profileSize = 128; + } + if (profileSize > 200) { + profileSize = 200; + } + } + + return SliverToBoxAdapter( + child: Center( + child: Column( + children: [ + const SizedBox(height: 64), + const TitleText(text: 'Sofie Doe'), + const SizedBox(height: 12), + SizedBox( + width: profileSize, + height: profileSize, + child: ClipOval( + child: Image.asset( + 'assets/user.jpg', + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + const Text( + 'Trips', + style: TextStyle( + fontSize: 14, + color: AppColors.textGrey, + ), + ), + Text( + '$_trips', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(width: 32), + Column( + children: [ + const Text( + 'Countries', + style: TextStyle( + fontSize: 14, + color: AppColors.textGrey, + ), + ), + Text( + '$_countriesVisited', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + }, + ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 32), + _SectionTile( + title: 'Settings', + subtitle: 'Notifications', + trailing: NotificationSwitch( + notificationsEnabled: _notificationsEnabled, + onChanged: (value) { + setState(() { + _notificationsEnabled = value; + }); + }, + ), + ), + Link( + uri: Uri.parse('https://flutter.dev'), + builder: (BuildContext context, FollowLink? followLink) { + return _SectionTile( + title: 'Support', + subtitle: 'Flutter.dev', + onTap: followLink, + ); + }, + ), + _SectionTile( + title: 'Birthdate', + subtitle: + _selectedBirthdate == null + ? 'Not set' + : DateFormat.yMMMd().format(_selectedBirthdate!), + onTap: () => _selectDate(context), + ), + _SectionTile( + title: 'Sign Out', + subtitle: 'Sign out of your account', + onTap: () { + context.read().logout(); + }, + ), + const SizedBox(height: 64), + ], + ), + ), + ], + ), + ); + } +} + +class _SectionTile extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? trailing; + final Widget? child; + final VoidCallback? onTap; + + const _SectionTile({ + required this.title, + this.subtitle, + this.trailing, + // ignore: unused_element_parameter + this.child, + this.onTap, + }); + + static const TextStyle _titleStyle = TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black, + ); + static const TextStyle _subtitleStyle = TextStyle( + color: AppColors.textGrey, + fontSize: 14, + ); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + splashFactory: NoSplash.splashFactory, + highlightColor: Colors.grey.shade300, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 2.0), + child: Text(title, style: _titleStyle), + ), + if (subtitle != null) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Text(subtitle!, style: _subtitleStyle), + ), + if (child != null) child!, + ], + ), + ), + if (trailing != null) + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: trailing!, + ), + ], + ), + ), + ); + } +} + +class NotificationSwitch extends StatelessWidget { + final bool notificationsEnabled; + final ValueChanged onChanged; + + const NotificationSwitch({ + super.key, + required this.notificationsEnabled, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isIOS) { + return CupertinoSwitch(value: notificationsEnabled, onChanged: onChanged); + } else { + return Switch(value: notificationsEnabled, onChanged: onChanged); + } + } +} + +final CupertinoThemeData _defaultCupertinoThemeData = CupertinoThemeData.raw( + null, + null, + null, + null, + null, + null, + null, + null, +); diff --git a/compass_25/lib/screens/search.dart b/compass_25/lib/screens/search.dart new file mode 100644 index 0000000..9839240 --- /dev/null +++ b/compass_25/lib/screens/search.dart @@ -0,0 +1,570 @@ +import 'package:cupertino_compass/router.dart'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:provider/provider.dart'; + +import '../adaptive/widgets.dart' as adaptive; +import '../model/model.dart'; +import '../theme.dart'; +import '../utils.dart'; + +class Search { + final Continent? continent; + final Location? location; + final DateTimeRange? dateTimeRange; + final int? guests; + + Search({this.continent, this.dateTimeRange, this.guests, this.location}); + + @override + String toString() { + return 'Search(continent: $continent, location: $location, dateTimeRange: $dateTimeRange, guests: $guests)'; + } +} + +class SearchScreen extends StatefulWidget { + final ValueChanged onSearch; + const SearchScreen({super.key, required this.onSearch}); + + @override + State createState() => _SearchScreenState(); +} + +class _SearchScreenState extends State { + final _formKey = GlobalKey(); + DateTimeRange? dateTimeRange; + int guests = 1; + Continent? continent; + Location? location; + + bool get isValid => dateTimeRange != null && guests > 0; + + void _submitForm() { + if (_formKey.currentState!.validate()) { + // If no location is selected, use the continent's location + Location? finalLocation = location; + if (finalLocation == null && continent != null) { + finalLocation = continent!.location; + } + + widget.onSearch( + Search( + continent: continent, + location: finalLocation, + dateTimeRange: dateTimeRange, + guests: guests, + ), + ); + navigatorKey.currentState?.pop(); + } else { + // Handle invalid form + } + } + + void _clearSearch() { + widget.onSearch(Search()); + navigatorKey.currentState?.pop(); + } + + Continent? findClosestContinent( + Location location, + List continents, + ) { + if (continents.isEmpty) { + return null; + } + + return continents.reduce((a, b) { + final distanceA = Geolocator.distanceBetween( + location.latitude, + location.longitude, + a.location.latitude, + a.location.longitude, + ); + final distanceB = Geolocator.distanceBetween( + location.latitude, + location.longitude, + b.location.latitude, + b.location.longitude, + ); + return distanceA < distanceB ? a : b; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + actions: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + navigatorKey.currentState?.pop(); + }, + ), + ], + ), + body: SafeArea( + child: Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 64), + child: Text( + "Search for activities across the world", + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + SearchFormDate( + onDateRangeChanged: (DateTimeRange? value) { + setState(() { + dateTimeRange = value; + }); + }, + ), + SearchFormGuests( + guests: guests, + onGuestsChanged: (guests) { + setState(() { + this.guests = guests; + }); + }, + ), + SearchFormLocation( + continent: continent, + location: location, + onLocationChanged: (location) { + setState(() { + this.location = location; + if (location != null) { + final closestContinent = findClosestContinent( + location, + context.read().continents, + ); + if (closestContinent != null) { + continent = closestContinent; + } + } + }); + }, + ), + SearchFormSubmit(valid: isValid, onPressed: _submitForm), + TextButton( + onPressed: _clearSearch, + child: const Text( + 'Clear Search', + style: TextStyle(color: Colors.grey), + ), + ), + ], + ), + ), + ), + ); + } +} + +class SearchFormDate extends FormField { + SearchFormDate({ + super.key, + required ValueChanged onDateRangeChanged, + }) : super( + initialValue: null, + builder: (FormFieldState state) { + return _SearchFormDateInner( + onDateRangeChanged: (DateTimeRange? value) { + state.didChange(value); + onDateRangeChanged(value); + }, + dateRange: state.value, + ); + }, + ); +} + +class _SearchFormDateInner extends StatefulWidget { + const _SearchFormDateInner({ + required this.onDateRangeChanged, + required this.dateRange, + }); + + final ValueChanged onDateRangeChanged; + final DateTimeRange? dateRange; + + @override + State<_SearchFormDateInner> createState() => _SearchFormDateInnerState(); +} + +class _SearchFormDateInnerState extends State<_SearchFormDateInner> { + DateTimeRange? _dateRange; + + @override + void initState() { + super.initState(); + _dateRange = widget.dateRange; + } + + @override + Widget build(BuildContext ontext) { + return Padding( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: _GrayscaleTapEffect( + onTap: () async { + final dateRange = await showDateRangePicker( + context: context, + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (dateRange != null) { + setState(() { + _dateRange = dateRange; + }); + widget.onDateRangeChanged(dateRange); + } + }, + child: Container( + height: 64, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(16.0), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('When', style: Theme.of(context).textTheme.titleMedium), + if (_dateRange != null) + Text( + dateFormatStartEnd(_dateRange!), + style: Theme.of(context).textTheme.bodyLarge, + ) + else + Text( + "Add Dates", + style: Theme.of(context).inputDecorationTheme.hintStyle, + ), + ], + ), + ), + ), + ), + ); + } +} + +const String removeGuestsKey = 'remove-guests'; +const String addGuestsKey = 'add-guests'; + +class SearchFormGuests extends FormField { + SearchFormGuests({ + super.key, + required int guests, + required ValueChanged onGuestsChanged, + }) : super( + initialValue: guests, + builder: (FormFieldState state) { + return _SearchFormGuestsInner( + guests: state.value!, + onGuestsChanged: (guests) { + state.didChange(guests); + onGuestsChanged(guests); + }, + ); + }, + ); +} + +class _SearchFormGuestsInner extends StatelessWidget { + const _SearchFormGuestsInner({ + required this.guests, + required this.onGuestsChanged, + }); + + final int guests; + final ValueChanged onGuestsChanged; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: Container( + height: 64, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Number of guests', + style: Theme.of(context).textTheme.titleMedium, + ), + _QuantitySelector( + guests: guests, + onGuestsChanged: onGuestsChanged, + ), + ], + ), + ), + ), + ); + } +} + +class _QuantitySelector extends StatelessWidget { + const _QuantitySelector({ + required this.guests, + required this.onGuestsChanged, + }); + + final int guests; + final ValueChanged onGuestsChanged; + + @override + Widget build(BuildContext context) { + const Color grey = Colors.grey; + const double buttonSize = 48; + const double borderRadius = 16; + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(borderRadius), + child: SizedBox( + width: buttonSize, + height: buttonSize, + child: InkWell( + // Keeping InkWell for the +/- buttons' feedback + key: const ValueKey(removeGuestsKey), + borderRadius: BorderRadius.circular(borderRadius), + splashFactory: NoSplash.splashFactory, + onTap: () { + if (guests > 0) { + onGuestsChanged(guests - 1); + } + }, + child: const Icon(Icons.remove_circle_outline, color: grey), + ), + ), + ), + SizedBox( + width: 36, + child: Center( + child: Text( + guests.toString(), + style: + guests == 0 + ? Theme.of(context).inputDecorationTheme.hintStyle + : Theme.of(context).textTheme.bodyMedium, + ), + ), + ), + Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(borderRadius), + child: SizedBox( + width: buttonSize, + height: buttonSize, + child: InkWell( + // Keeping InkWell for the +/- buttons' feedback + key: const ValueKey(addGuestsKey), + borderRadius: BorderRadius.circular(borderRadius), + splashFactory: NoSplash.splashFactory, + onTap: () { + onGuestsChanged(guests + 1); + }, + child: const Icon(Icons.add_circle_outline, color: grey), + ), + ), + ), + ], + ); + } +} + +class SearchFormSubmit extends StatelessWidget { + const SearchFormSubmit({ + super.key, + required this.valid, + required this.onPressed, + }); + + final bool valid; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 16), + child: SizedBox( + height: 52, + child: adaptive.Button( + color: AppColors.primary, + onPressed: valid ? onPressed : null, + child: const Center( + child: Text('Search', style: TextStyle(color: Colors.white)), + ), + ), + ), + ); + } +} + +class SearchFormLocation extends FormField { + SearchFormLocation({ + super.key, + final Location? location, + required ValueChanged onLocationChanged, + required Continent? continent, + }) : super( + initialValue: location, + builder: (FormFieldState state) { + return _SearchFormLocationInner( + onLocationChanged: (location) { + state.didChange(location); + onLocationChanged(location); + }, + currentLocation: state.value, + continent: continent, + ); + }, + ); +} + +class _SearchFormLocationInner extends StatefulWidget { + const _SearchFormLocationInner({ + required this.onLocationChanged, + this.currentLocation, + required this.continent, + }); + + final ValueChanged onLocationChanged; + final Location? currentLocation; + final Continent? continent; + + @override + State<_SearchFormLocationInner> createState() => + _SearchFormLocationInnerState(); +} + +class _SearchFormLocationInnerState extends State<_SearchFormLocationInner> { + Location? _currentLocation; + + @override + void initState() { + super.initState(); + _currentLocation = widget.currentLocation; + } + + Future _getCurrentLocation() async { + final hasPermission = await _checkLocationPermission(); + if (!hasPermission) { + return; + } + + final position = await Geolocator.getCurrentPosition(); + _setLocation(position); + } + + void _setLocation(Position position) { + setState(() { + _currentLocation = Location(position.latitude, position.longitude); + widget.onLocationChanged(_currentLocation); + }); + } + + Future _checkLocationPermission() async { + final hasPermission = await Geolocator.isLocationServiceEnabled(); + if (!hasPermission) { + return false; + } + + var permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + if (permission == LocationPermission.denied || + permission == LocationPermission.deniedForever) { + return false; + } + + if (!hasPermission) { + return false; + } + + return true; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: _GrayscaleTapEffect( + onTap: _getCurrentLocation, + borderRadius: BorderRadius.circular(16), + child: Container( + height: 64, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: Text( + 'Where', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + _currentLocation == null + ? const Icon(Icons.location_on) + : const Icon(Icons.check_circle_outline), + ], + ), + ), + ), + ), + ); + } +} + +class _GrayscaleTapEffect extends StatefulWidget { + final Widget child; + final VoidCallback? onTap; + final BorderRadius? borderRadius; + + const _GrayscaleTapEffect({ + required this.child, + this.onTap, + this.borderRadius, + }); + + @override + State<_GrayscaleTapEffect> createState() => _GrayscaleTapEffectState(); +} + +class _GrayscaleTapEffectState extends State<_GrayscaleTapEffect> { + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: widget.borderRadius, + onTap: widget.onTap, + splashFactory: NoSplash.splashFactory, + child: widget.child, + ), + ); + } +} diff --git a/compass_25/lib/theme.dart b/compass_25/lib/theme.dart new file mode 100644 index 0000000..82b5c1e --- /dev/null +++ b/compass_25/lib/theme.dart @@ -0,0 +1,121 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +abstract final class AppColors { + static const primary = primarySeedColor; + static const red1 = Color(0xFFE74C3C); + static const grey3 = Color(0xFFA4A4A4); + static const textGrey = Color(0xFF4D4D4D); + static const whiteTransparent = Color(0x4DFFFFFF); + static WidgetStateProperty primaryColorMaterialState = + WidgetStateProperty.fromMap({ + WidgetState.focused | WidgetState.pressed | WidgetState.hovered: + AppColors.primary, + WidgetState.disabled: Colors.grey.shade400, + WidgetState.any: AppColors.primary, + }); + static const chatBubbleUser = Color(0xFFE9DFF8); + static const chatBubbleOther = Color(0xFFEDE7F1); +} + +const primarySeedColor = Color(0xFF660091); +const surfaceTintColorValue = Colors.grey; + +ThemeData lightTheme = ThemeData( + appBarTheme: AppBarTheme( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + scrolledUnderElevation: 2.0, + shadowColor: Colors.grey, + ), + colorScheme: ColorScheme.fromSeed( + seedColor: primarySeedColor, + brightness: Brightness.light, + onSurface: Colors.black, + primaryContainer: Colors.grey.shade400, + ).copyWith(surface: Colors.white), + buttonTheme: ButtonThemeData(buttonColor: AppColors.primary), + primaryColor: primarySeedColor, + scaffoldBackgroundColor: Colors.white, + useMaterial3: true, + textTheme: TextTheme( + displayLarge: GoogleFonts.rubik(), + displayMedium: GoogleFonts.rubik(), + displaySmall: GoogleFonts.rubik(), + headlineLarge: GoogleFonts.rubik(), + headlineMedium: GoogleFonts.rubik(), + headlineSmall: GoogleFonts.rubik(), + titleLarge: GoogleFonts.rubik(), + titleMedium: GoogleFonts.rubik(), + titleSmall: GoogleFonts.rubik(), + bodyLarge: GoogleFonts.rubik(), + bodyMedium: GoogleFonts.rubik(), + bodySmall: GoogleFonts.rubik(), + labelLarge: GoogleFonts.rubik(), + labelMedium: GoogleFonts.rubik(), + labelSmall: GoogleFonts.rubik(), + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Colors.white, + ), + cupertinoOverrideTheme: CupertinoThemeData( + textTheme: CupertinoTextThemeData( + textStyle: GoogleFonts.rubik(), + actionTextStyle: GoogleFonts.rubik(color: primarySeedColor), + actionSmallTextStyle: GoogleFonts.rubik( + color: primarySeedColor, + fontSize: 10, + ), + pickerTextStyle: GoogleFonts.rubik(), + dateTimePickerTextStyle: GoogleFonts.rubik(), + tabLabelTextStyle: GoogleFonts.rubik(fontSize: 12), + ), + ), +); + +ThemeData darkTheme = ThemeData( + appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0), + colorScheme: ColorScheme.fromSeed( + seedColor: primarySeedColor, + brightness: Brightness.dark, + ), + primaryColor: primarySeedColor, + useMaterial3: true, + textTheme: TextTheme( + displayLarge: GoogleFonts.rubik(color: Colors.white), + displayMedium: GoogleFonts.rubik(color: Colors.white), + displaySmall: GoogleFonts.rubik(color: Colors.white), + headlineLarge: GoogleFonts.rubik(color: Colors.white), + headlineMedium: GoogleFonts.rubik(color: Colors.white), + headlineSmall: GoogleFonts.rubik(color: Colors.white), + titleLarge: GoogleFonts.rubik(color: Colors.white), + titleMedium: GoogleFonts.rubik(color: Colors.white), + titleSmall: GoogleFonts.rubik(color: Colors.white), + bodyLarge: GoogleFonts.rubik(color: Colors.white), + bodyMedium: GoogleFonts.rubik(color: Colors.white), + bodySmall: GoogleFonts.rubik(color: Colors.white), + labelLarge: GoogleFonts.rubik(color: Colors.white), + labelMedium: GoogleFonts.rubik(color: Colors.white), + labelSmall: GoogleFonts.rubik(color: Colors.white), + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Colors.white, + ), + cupertinoOverrideTheme: CupertinoThemeData( + textTheme: CupertinoTextThemeData( + textStyle: GoogleFonts.rubik(color: Colors.white), + actionTextStyle: GoogleFonts.rubik(color: primarySeedColor), + navTitleTextStyle: GoogleFonts.rubik( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + pickerTextStyle: GoogleFonts.rubik(color: Colors.white), + dateTimePickerTextStyle: GoogleFonts.rubik(color: Colors.white), + ), + ), +); diff --git a/compass_25/lib/utils.dart b/compass_25/lib/utils.dart new file mode 100644 index 0000000..07f8060 --- /dev/null +++ b/compass_25/lib/utils.dart @@ -0,0 +1,37 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +String dateFormatStartEnd(DateTimeRange dateTimeRange) { + final start = dateTimeRange.start; + final end = dateTimeRange.end; + + String addOrdinal(int day) { + if (day >= 11 && day <= 13) { + return '${day}th'; + } + switch (day % 10) { + case 1: + return '${day}st'; + case 2: + return '${day}nd'; + case 3: + return '${day}rd'; + default: + return '${day}th'; + } + } + + final dayStart = addOrdinal(start.day); + final monthStart = DateFormat('MMMM').format(start); + final dayMonthStart = '$dayStart $monthStart'; + + final dayEnd = addOrdinal(end.day); + final monthEnd = DateFormat('MMMM').format(end); + final dayMonthEnd = '$dayEnd $monthEnd'; + + return '$dayMonthStart - $dayMonthEnd'; +} diff --git a/compass_25/lib/widgets/activity.dart b/compass_25/lib/widgets/activity.dart new file mode 100644 index 0000000..e2e4de3 --- /dev/null +++ b/compass_25/lib/widgets/activity.dart @@ -0,0 +1,88 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +import '../model/activity.dart'; +import '../theme.dart'; +import 'checkbox.dart'; + +class ActivityEntry extends StatelessWidget { + const ActivityEntry({ + super.key, + required this.showCheckbox, + required this.activity, + this.selected = false, + this.onChanged, + }); + + final bool showCheckbox; + final Activity activity; + final bool selected; + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 96, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 10.0, right: 20), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: activity.imageUrl, + height: 80, + width: 80, + fadeInDuration: Duration(milliseconds: 200), + fit: BoxFit.cover, + placeholder: (context, url) { + return Container( + color: Colors.grey[300], + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + ); + }, + ), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + activity.timeOfDay.name.toUpperCase(), + style: Theme.of(context).textTheme.labelSmall?.copyWith(fontSize: 10, color: Colors.grey.shade500, fontWeight: FontWeight.bold), + ), + Text( + activity.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16), + ), + ], + ), + ), + const SizedBox(width: 20), + if (showCheckbox) + Padding( + padding: const EdgeInsets.all(10.0), + child: CustomCheckbox( + key: ValueKey('${activity.ref}-checkbox'), + value: selected, + backgroundColor: AppColors.primary, + onChanged: onChanged ?? (b) {}, + ), + ), + ], + ), + ), + ); + } +} diff --git a/compass_25/lib/widgets/back_button.dart b/compass_25/lib/widgets/back_button.dart new file mode 100644 index 0000000..625dfde --- /dev/null +++ b/compass_25/lib/widgets/back_button.dart @@ -0,0 +1,66 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class AdaptiveBackButton extends StatelessWidget { + final VoidCallback onPressed; + const AdaptiveBackButton({super.key, required this.onPressed}); + + @override + Widget build(BuildContext context) { + if (!Platform.isIOS) { + return _buildAndroidBackButton(context); + } else { + return ClipOval( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + width: 38, + height: 38, + color: Colors.white.withValues(alpha: 0.2), + child: Center( + child: SizedBox( + width: 40, + height: 40, + child: CupertinoButton( + padding: EdgeInsets.zero, + borderRadius: BorderRadius.circular(30), + onPressed: () { + onPressed(); + }, + child: const Icon(CupertinoIcons.xmark, color: Colors.black), + ), + ), + ), + ), + ), + ); + } + } + + Widget _buildAndroidBackButton(BuildContext context) { + return SizedBox( + height: 40.0, + width: 40.0, + child: Material( + color: Color(0xCCFFFFFF), + borderRadius: BorderRadius.circular(8.0), + child: InkWell( + borderRadius: BorderRadius.circular(8.0), + onTap: () { + onPressed(); + }, + child: const Center( + child: Icon(Icons.arrow_back, size: 24.0, color: Colors.black), + ), + ), + ), + ); + } +} diff --git a/compass_25/lib/widgets/chat_bubble.dart b/compass_25/lib/widgets/chat_bubble.dart new file mode 100644 index 0000000..1494122 --- /dev/null +++ b/compass_25/lib/widgets/chat_bubble.dart @@ -0,0 +1,127 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'dart:io' show Platform; + +class AdaptiveChatBubble extends StatelessWidget { + final String message; + final bool isUser; + final Color iOSBackgroundColor; + final Color iOSTextColor; + final Color androidBackgroundColor; + final Color androidTextColor; + + const AdaptiveChatBubble({ + super.key, + required this.message, + required this.isUser, + this.iOSBackgroundColor = Colors.blue, + this.iOSTextColor = Colors.white, + this.androidBackgroundColor = Colors.green, + this.androidTextColor = Colors.white, + }); + + @override + Widget build(BuildContext context) { + if (kIsWeb || Platform.isIOS) { + return _buildIOSBubble(context); + } else if (Platform.isAndroid) { + return _buildAndroidBubble(context); + } else { + return _buildIOSBubble(context); + } + } + + Widget _buildIOSBubble(BuildContext context) { + return Align( + alignment: isUser ? Alignment.topRight : Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: CustomPaint( + painter: ChatBubblePainter(color: iOSBackgroundColor, isUser: isUser), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.7, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), // More rounded for iOS + ), + child: Text(message, style: TextStyle(color: iOSTextColor)), + ), + ), + ), + ); + } + + Widget _buildAndroidBubble(BuildContext context) { + return Align( + alignment: isUser ? Alignment.topRight : Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.7, + ), + decoration: BoxDecoration( + color: androidBackgroundColor, + borderRadius: BorderRadius.circular(8), // Less rounded for Android + ), + child: Text(message, style: TextStyle(color: androidTextColor)), + ), + ), + ); + } +} + +class ChatBubblePainter extends CustomPainter { + final Color color; + final bool isUser; + + ChatBubblePainter({required this.color, required this.isUser}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + final path = Path(); + + if (isUser) { + path.moveTo(size.width - 15, size.height); + path.quadraticBezierTo( + size.width, + size.height, + size.width, + size.height - 10, + ); + path.lineTo(size.width, 10); + path.quadraticBezierTo(size.width, 0, size.width - 10, 0); + path.lineTo(10, 0); + path.quadraticBezierTo(0, 0, 0, 10); + path.lineTo(0, size.height - 10); + path.quadraticBezierTo(0, size.height, 10, size.height); + path.lineTo(size.width - 15, size.height); + } else { + path.moveTo(15, size.height); + path.quadraticBezierTo(0, size.height, 0, size.height - 10); + path.lineTo(0, 10); + path.quadraticBezierTo(0, 0, 10, 0); + path.lineTo(size.width - 10, 0); + path.quadraticBezierTo(size.width, 0, size.width, 10); + path.lineTo(size.width, size.height - 10); + path.quadraticBezierTo( + size.width, + size.height, + size.width - 10, + size.height, + ); + path.lineTo(15, size.height); + } + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } +} diff --git a/compass_25/lib/widgets/checkbox.dart b/compass_25/lib/widgets/checkbox.dart new file mode 100644 index 0000000..59f39eb --- /dev/null +++ b/compass_25/lib/widgets/checkbox.dart @@ -0,0 +1,53 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +class CustomCheckbox extends StatelessWidget { + const CustomCheckbox({ + super.key, + required this.value, + required this.onChanged, + this.backgroundColor, + }); + + final bool value; + final ValueChanged onChanged; + final Color? backgroundColor; + + @override + Widget build(BuildContext context) { + return InkResponse( + radius: 24, + onTap: () => onChanged(!value), + splashColor: Colors.transparent, // Remove splash effect + highlightColor: Colors.transparent, // Remove highlight effect + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.grey.shade300), + ), + child: Material( + borderRadius: BorderRadius.circular(24), + color: + value + ? backgroundColor ?? Theme.of(context).colorScheme.primary + : Colors.transparent, + child: SizedBox( + width: 24, + height: 24, + child: Visibility( + visible: value, + child: Icon( + Icons.check, + size: 18, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/compass_25/lib/widgets/destination.dart b/compass_25/lib/widgets/destination.dart new file mode 100644 index 0000000..2a98d20 --- /dev/null +++ b/compass_25/lib/widgets/destination.dart @@ -0,0 +1,77 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:transparent_image/transparent_image.dart'; + +import '../model/destination.dart'; + +class DestinationWidget extends StatelessWidget { + final Destination destination; + final VoidCallback onTap; + + const DestinationWidget({ + required this.destination, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Card( + clipBehavior: Clip.antiAlias, + color: Colors.white, + elevation: Platform.isIOS ? 80 : 3, + shadowColor: Platform.isIOS ? Color(0xFAFAFAFA) : Colors.grey.shade400, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ColoredBox( + color: Colors.black12, + child: FadeInImage.memoryNetwork( + fadeInDuration: Duration(milliseconds: 100), + fadeOutDuration: Duration(milliseconds: 100), + image: destination.imageUrl, + placeholder: kTransparentImage, + fit: BoxFit.fitWidth, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 18, + vertical: 15, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + destination.name, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + destination.continent, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ], + ), + InkWell(splashColor: Colors.black12, onTap: onTap), + ], + ), + ); + } +} diff --git a/compass_25/lib/widgets/tag_chip.dart b/compass_25/lib/widgets/tag_chip.dart new file mode 100644 index 0000000..74a2ee7 --- /dev/null +++ b/compass_25/lib/widgets/tag_chip.dart @@ -0,0 +1,141 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import '../theme.dart'; + +class TagChip extends StatelessWidget { + const TagChip({ + super.key, + required this.tag, + this.fontSize = 10, + this.height = 20, + this.chipColor, + this.onChipColor, + }); + + final String tag; + + final double fontSize; + final double height; + final Color? chipColor; + final Color? onChipColor; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(height / 2), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: DecoratedBox( + decoration: BoxDecoration( + color: + chipColor ?? + Theme.of(context).extension()?.chipColor ?? + AppColors.whiteTransparent, + ), + child: SizedBox( + height: height, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _iconFrom(tag), + color: + onChipColor ?? + Theme.of( + context, + ).extension()?.onChipColor ?? + Colors.white, + size: fontSize, + ), + const SizedBox(width: 4), + Text( + tag, + textAlign: TextAlign.center, + style: _textStyle(context), + ), + ], + ), + ), + ), + ), + ), + ); + } + + IconData? _iconFrom(String tag) { + return switch (tag) { + 'Adventure sports' => Icons.kayaking_outlined, + 'Beach' => Icons.beach_access_outlined, + 'City' => Icons.location_city_outlined, + 'Cultural experiences' => Icons.museum_outlined, + 'Foodie' || 'Food tours' => Icons.restaurant, + 'Hiking' => Icons.hiking, + 'Historic' => Icons.menu_book_outlined, + 'Island' || 'Coastal' || 'Lake' || 'River' => Icons.water, + 'Luxury' => Icons.attach_money_outlined, + 'Mountain' || 'Wildlife watching' => Icons.landscape_outlined, + 'Nightlife' => Icons.local_bar_outlined, + 'Off-the-beaten-path' => Icons.do_not_step_outlined, + 'Romantic' => Icons.favorite_border_outlined, + 'Rural' => Icons.agriculture_outlined, + 'Secluded' => Icons.church_outlined, + 'Sightseeing' => Icons.attractions_outlined, + 'Skiing' => Icons.downhill_skiing_outlined, + 'Wine tasting' => Icons.wine_bar_outlined, + 'Winter destination' => Icons.ac_unit, + _ => Icons.label_outlined, + }; + } + + TextStyle _textStyle(BuildContext context) => + TextTheme.of(context).bodyLarge!.copyWith( + fontWeight: FontWeight.w500, + fontSize: fontSize, + color: + onChipColor ?? + Theme.of(context).extension()?.onChipColor ?? + Colors.white, + textBaseline: TextBaseline.alphabetic, + ); +} + +class TagChipTheme extends ThemeExtension { + final Color chipColor; + final Color onChipColor; + + TagChipTheme({required this.chipColor, required this.onChipColor}); + + @override + ThemeExtension copyWith({ + Color? chipColor, + Color? onChipColor, + }) { + return TagChipTheme( + chipColor: chipColor ?? this.chipColor, + onChipColor: onChipColor ?? this.onChipColor, + ); + } + + @override + ThemeExtension lerp( + covariant ThemeExtension other, + double t, + ) { + if (other is! TagChipTheme) { + return this; + } + return TagChipTheme( + chipColor: Color.lerp(chipColor, other.chipColor, t) ?? chipColor, + onChipColor: Color.lerp(onChipColor, other.onChipColor, t) ?? onChipColor, + ); + } +} diff --git a/compass_25/lib/widgets/title_text.dart b/compass_25/lib/widgets/title_text.dart new file mode 100644 index 0000000..68c0bbf --- /dev/null +++ b/compass_25/lib/widgets/title_text.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class TitleText extends StatelessWidget { + final String text; + final double? size; + const TitleText({super.key, required this.text, this.size}); + + @override + Widget build(BuildContext context) { + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: + (bounds) => RadialGradient( + center: Alignment.bottomLeft, + radius: 2, + focalRadius: 0, + colors: [Color(0xFF59B7EC), Color(0xFF9A62E1), Color(0xFFE66CF9)], + ).createShader( + Rect.fromLTWH(0, 0, bounds.width * 3, bounds.height * 3), + ), + child: Text( + text, + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontWeight: FontWeight.bold, + fontSize: size ?? 32, + ), + ), + ); + } +} diff --git a/compass_25/linux/.gitignore b/compass_25/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/compass_25/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/compass_25/linux/CMakeLists.txt b/compass_25/linux/CMakeLists.txt new file mode 100644 index 0000000..c45ef75 --- /dev/null +++ b/compass_25/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "cupertino_compass") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.cupertino_compass") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/compass_25/linux/flutter/CMakeLists.txt b/compass_25/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/compass_25/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/compass_25/linux/flutter/generated_plugin_registrant.cc b/compass_25/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..f6f23bf --- /dev/null +++ b/compass_25/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/compass_25/linux/flutter/generated_plugin_registrant.h b/compass_25/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/compass_25/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/compass_25/linux/flutter/generated_plugins.cmake b/compass_25/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..f16b4c3 --- /dev/null +++ b/compass_25/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/compass_25/linux/runner/CMakeLists.txt b/compass_25/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/compass_25/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/compass_25/linux/runner/main.cc b/compass_25/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/compass_25/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/compass_25/linux/runner/my_application.cc b/compass_25/linux/runner/my_application.cc new file mode 100644 index 0000000..1b38957 --- /dev/null +++ b/compass_25/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "cupertino_compass"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "cupertino_compass"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/compass_25/linux/runner/my_application.h b/compass_25/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/compass_25/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/compass_25/pubspec.yaml b/compass_25/pubspec.yaml new file mode 100644 index 0000000..02b627f --- /dev/null +++ b/compass_25/pubspec.yaml @@ -0,0 +1,47 @@ +name: cupertino_compass +description: "A travel planning app built with Flutter" +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: ^3.7.0 + +dependencies: + animations: any + cached_network_image: any + cloud_firestore: ^5.6.6 + collection: any + cupertino_icons: any + firebase_auth: ^5.5.2 + firebase_core: ^3.13.0 + geolocator: ^11.0.0 + go_router: any + google_fonts: any + intl: any + json_annotation: any + provider: any + transparent_image: any + url_launcher: any + flutter: + sdk: flutter + +dev_dependencies: + json_serializable: any + build_runner: any + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true + assets: + - assets/google_fonts/ + - assets/icons/ + - assets/activities.json + - assets/destinations.json + - assets/icon.png + - assets/logo.png + - assets/logotype.png + - assets/login_bg.png + - assets/user.jpg + - assets/title.png diff --git a/crossword_companion/.gitignore b/crossword_companion/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/crossword_companion/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/crossword_companion/.metadata b/crossword_companion/.metadata new file mode 100644 index 0000000..b9fd747 --- /dev/null +++ b/crossword_companion/.metadata @@ -0,0 +1,30 @@ +# 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: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: ios + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # 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' diff --git a/crossword_companion/GEMINI.md b/crossword_companion/GEMINI.md new file mode 100644 index 0000000..2dcce8f --- /dev/null +++ b/crossword_companion/GEMINI.md @@ -0,0 +1,250 @@ +# Gemini Code-Gen Best Practices for This Project + +This document outlines the best practices and coding standards to be followed +during the development of this Flutter project. Adhering to these guidelines +will ensure the codebase is clean, maintainable, and scalable. + +## Architectural Principles + +- **DRY (Don’t Repeat Yourself)** – eliminate duplicated logic by extracting + shared utilities and modules. +- **Separation of Concerns** – each module should handle one distinct + responsibility. +- **Single Responsibility Principle (SRP)** – every class/module/function/file + should have exactly one reason to change. +- **Clear Abstractions & Contracts** – expose intent through small, stable + interfaces and hide implementation details. +- **Low Coupling, High Cohesion** – keep modules self-contained, minimize + cross-dependencies. +- **Scalability & Statelessness** – design components to scale horizontally and + prefer stateless services when possible. +- **Observability & Testability** – build in logging, metrics, tracing, and + ensure components can be unit/integration tested. +- **KISS (Keep It Simple, Sir)** - keep solutions as simple as possible. +- **YAGNI (You're Not Gonna Need It)** – avoid speculative complexity or + over-engineering. + +## Coding Standards + +### Linting +This project uses the standard set of lints provided by the `flutter_lints` +package. Ensure that all code adheres to these rules to maintain code quality +and consistency. Run `flutter analyze` frequently to check for linting issues. + +### Naming Conventions +- **Files:** Use `snake_case` for file names (e.g., `user_profile.dart`). +- **Classes:** Use `PascalCase` for classes (e.g., `UserProfile`). +- **Methods and Variables:** Use `camelCase` for methods and variables (e.g., + `getUserProfile`). +- **Constants:** Use `camelCase` for constants (e.g., `defaultTimeout`). + +### Cross-Platform Compatibility +This application targets Android, iOS, web, and macOS. All code must be written +to be platform-agnostic. + +- **Avoid Platform-Specific APIs:** Do not use platform-specific libraries or + APIs directly (e.g., `dart:io`'s `File` class for UI rendering). When + platform-specific code is unavoidable, it is abstracted away behind a common + interface using an adapter pattern, as seen in the `lib/platform` directory. +- **Use Flutter-Native Solutions:** Prefer Flutter's built-in, cross-platform + widgets and utilities (e.g., `Image.memory` with byte data for displaying + images from `image_picker`, which works on all platforms). +- **Verify Plugin Compatibility:** Before using a new package, ensure it + supports all target platforms (Android, iOS, web). + +### Don't Swallow Errors +- **Don't Swallow Errors** by catching expections, silently filling in required + but missing values or adding timeouts when something hangs unexpectedly. All + of those are exceptions that should be thrown so that the errors can be seen, + root causes can be found and fixes can be applied. +- **Use Assertions for Invariants:** Use `assert` statements to validate + assumptions and logical invariants in your code. For example, if a function + requires a list to be non-empty before proceeding, assert that condition at + the beginning of the function. This practice turns potential silent failures + into loud, immediate errors during development, making complex bugs + significantly easier to track down. + +### Null Value Handling +- Prefer using required parameters in constructors and methods when a value is + not expected to be null. +- When the compiler requires a non-null value and you are certain a value is not + null at that point, use the `!` (bang) operator. This turns invalid null + assumptions into runtime exceptions, making them easier to find and fix. +- Avoid providing default values for nullable types simply to satisfy the + compiler, as this can hide underlying data issues. + +### Widget Development +- **`const` Constructors:** Use `const` constructors for widgets whenever + possible to improve performance by allowing Flutter to cache and reuse widget + instances. +- **Break Down Large Widgets:** Decompose large widget build methods into + smaller, more manageable widgets. This improves readability, reusability, and + performance. + +### No Placeholder Code +- We're building production code here, not toys. Avoid placeholder code. + +### No Comments for Removed Functionality +- The source is not the place to keep a history of what's changed; it's the + place to implement the current requirements only. Use version control for + history. + +## Styling and Theming + +### Avoid Hardcoded Values +- **Do not** hardcode colors, dimensions, text styles, or other style values + directly in widgets. +- All centralized style-related code should be consolidated into + `lib/styles.dart`. +- Create descriptive, `camelCase` constants in a dedicated `lib/styles.dart` + file for any reusable style values that are not part of the main theme. + +### Theme Architecture +- The app uses Material Design 3 with a centralized theme defined in + `main.dart`. +- All UI components should inherit styles from this central theme. Avoid custom, + one-off styling for individual widgets. +- Only use per-widget theme or style overrides when a particular widget requires + a value that is explicitly different from the application-wide theme (e.g., a + special-purpose button with a unique color). + +#### Prioritize Blame Correctly +When debugging, assume the bug is in the local, new, application-specific code +before assuming a bug in a mature framework. + +## State Management +- **Provider:** use the provider package for state management + +## Testing +- Write unit tests for business logic (e.g., services, state management + controllers). +- Write widget tests to verify the UI and interactions of your widgets. +- Aim for a reasonable level of test coverage to ensure application stability + and prevent regressions. + +## Project Structure +- **`lib/`**: Contains all Dart code. + - **`main.dart`**: The application entry point and theme definition. + - **`styles.dart`**: Centralized file for style constants. + - **`models/`**: Directory for data model classes. + - `clue_answer.dart`: Model for a clue and its answer. + - `clue.dart`: Model for a single clue. + - `crossword_data.dart`: Model for the entire crossword puzzle data. + - `crossword_grid.dart`: Model for the crossword grid. + - `crossword_state.dart`: State management for the crossword puzzle. + - `grid_cell.dart`: Model for a single cell in the grid. + - `todo_item.dart`: (likely unused example code) + - **`platform/`**: Platform-specific implementations. + - `platform_io.dart`: IO-specific implementation. + - `platform_web.dart`: Web-specific implementation. + - `platform.dart`: Common platform interface. + - **`screens/`**: Top-level screen widgets. + - `crossword_screen.dart`: The main screen of the application. + - **`services/`**: Business logic services. + - `gemini_service.dart`: Service for interacting with the Gemini API. + - `image_picker_service.dart`: Service for picking images. + - `puzzle_solver.dart`: Service for solving the puzzle. + - **`widgets/`**: Reusable, shared widgets. + - `clue_list.dart`: Widget for displaying the list of clues. + - `grid_view.dart`: Widget for displaying the crossword grid. + - `step_state_base.dart`: Base class for step state management. + - `step1_select_image.dart`: Widget for the first step (selecting an image). + - `step2_verify_grid_size.dart`: Widget for the second step (verifying grid + size). + - `step3_verify_grid_contents.dart`: Widget for the third step (verifying + grid contents). + - `step4_verify_clue_text.dart`: Widget for the fourth step (verifying clue + text). + - `step5_solve_puzzle.dart`: Widget for the fifth step (solving the puzzle). + - `todo_list_widget.dart`: (likely unused example code) +- **`assets/`**: Contains static assets like images and fonts. +- **`test/`**: Contains tests for the application. +- **`web/`**: Contains web-specific files. +- **`macos/`**: Contains macOS-specific files. +- **`specs/`**: Contains project specifications and design documents. + +## Technical Accuracy and Verification + +To ensure the highest level of accuracy, the following verification steps are +mandatory when dealing with technical details like API names, library versions, +or other critical identifiers. + +1. **Prioritize Primary Sources:** Official documentation, API references, and + the project's own source code are the highest authority. Information from + secondary sources (e.g., blog posts, forum answers) must be cross-verified + against a primary source before being used. When a user provides a link to + official documentation, it must be treated as the ground truth. + +2. **Mandate Exact Identifier Verification:** When using a specific + identifier—such as a model name, package version, or function name—you must + find and use the **exact, literal string** from the primary source. Do not + shorten, paraphrase, or infer the name from surrounding text or titles. + +3. **Quote Before Use:** Before implementing a critical identifier obtained + from documentation, you must first quote the specific line or code block + from the source that confirms the identifier. This acts as a final + verification step to ensure you have found the precise value. + +## Project-Specific Implementation + +This Crossword Companion project serves as a practical example of the principles +outlined above: + +- **State Management:** The application uses the `provider` package for state + management, with a central `CrosswordState` class that acts as a + `ChangeNotifier`. This single source of truth manages the application's + data, such as the puzzle details and solver status. + +- **Event-Driven Navigation:** Step transitions are handled by a robust + two-phase state machine (`enteringStep`/`enteredStep`) within + `CrosswordState`. This allows each step widget to listen for when it is + being entered and run its own initialization logic in a self-contained + manner. + +- **Abstracted State Management:** To adhere to the DRY principle, the common + state management logic for each stepper page is encapsulated in a + `StepStateBase` abstract class. This base class handles the listener + registration and the two-phase state machine logic for entering a step. Each + step's state class then extends this base class and provides its `stepIndex` + and the specific logic to execute when the step is entered. + +- **Widget Decomposition:** The UI is broken down into small, single-purpose + widgets. For example, the main `CrosswordScreen` is composed of a `Stepper` + widget, which in turn uses a series of `Step...Content` widgets for each + step in the process. This makes the code more readable, reusable, and easier + to test. + +- **Centralized Theme:** The application's theme is defined in `main.dart` and + applied to the entire `MaterialApp`. This ensures a consistent look and feel + across all widgets and avoids hardcoded style values. + +- **Services:** Business logic is separated into a `GeminiService`. This + service is configured with a detailed system prompt that instructs the + `gemini-2.5-flash` model to act as a crossword-solving expert. This + decouples the UI from the underlying AI logic, making the code more modular + and easier to maintain. + +- **App-Driven Solving:** The puzzle-solving logic is not a simple API call + but an intelligent, app-driven loop managed by a dedicated `PuzzleSolver` + service, which is coordinated by `CrosswordState`. For each clue, the app + calculates the word's length and current letter pattern from the grid. It + then sends a highly focused prompt to the expert model. The app validates + the model's response, updates the grid, and automatically retries clues that + were answered incorrectly, creating a robust and resilient solving process. + +## Verification and Maintenance + +### Post-Change Verification +After any significant refactoring or feature addition, the following steps are +required to maintain code quality: + +1. **Run Static Analysis:** Execute `dart analyze` and fix all reported issues. +2. **Audit Against Best Practices:** Review the changes against the principles + outlined in the "Architectural Principles" and "Coding Standards" sections + of this document to ensure the code remains clean, robust, and maintainable. + +## Git Workflow + +- **Committing Changes:** After the changes are complete and verified, I will not + commit them to the repository. You, the user, are responsible for all git + commits. \ No newline at end of file diff --git a/crossword_companion/README.md b/crossword_companion/README.md new file mode 100644 index 0000000..244487f --- /dev/null +++ b/crossword_companion/README.md @@ -0,0 +1,90 @@ +# Crossword Companion + +The Crossword Companion is a Flutter sample app demonstrating an intelligent, +app-driven workflow using Flutter and the Google Gemini API through Firebase. +The app allows users to take or upload a picture of a crossword puzzle, verifies +the puzzle's structure and clues with the user, and then uses Gemini to solve it + in real-time. + +This project is an open-source sample intended to showcase how easy it is to +build an AI-powered app in Flutter beyond simple chat, allowing the user to step +in and direct the model as appropriate. + +The Crossword Companion app is supported where Firebase is support: Android, +iOS, web and macOS. + +## How It Works + +The application uses a multi-modal Gemini model (`gemini-2.5-pro`) to analyze an +image of a crossword puzzle. It then uses a separate model (`gemini-2.5-flash`), +configured with a detailed system prompt to act as a crossword "expert", to +solve the puzzle. Additionally, the app integrates with an external dictionary +API [dictionaryapi.dev](https://dictionaryapi.dev) to provide word metadata +(e.g., part of speech) when requested by the Gemini model during the solving +process. This integration allows the Gemini model to verify grammatical +constraints, such as part of speech, for potential answers, thereby improving +the accuracy and relevance of its solutions. + +The app itself drives the solving process. For each clue, it determines the +required word length and the current known letter pattern from the grid. It then +sends this focused context to the expert model. The app validates the answer, +updates the grid, and automatically retries clues that were answered +incorrectly, creating a robust feedback loop. + + + +## Getting Started + +### Prerequisites + +- The [Flutter SDK](https://docs.flutter.dev/install) installed. + +- A [Firebase project enabled for + Generative AI](https://firebase.google.com/docs/ai-logic/get-started?api=dev). + +### Installation + +1. Clone the repository. +2. Configure your Firebase project by running the following command at the + project root and following the instructions: + + ```bash + flutterfire config + ``` + + This will connect your Flutter application to your Firebase project, which + is necessary to use the Gemini API. + +3. Run the application on your desired platform: + + ```bash + flutter run + ``` + +## Functionality + +This application guides the user through a step-by-step workflow to solve a +crossword puzzle from an image. + +1. **Select Crossword Image:** The user can select an image of a crossword + puzzle from their device's gallery or by taking a photo. + +2. **Verify Grid Size:** The application uses Gemini to infer the information + about the crossword. On this step, the app shows the inferred grid + dimensions (width and height) and allows the user to make corrections. + +3. **Verify Grid Contents:** The app displays the inferred grid and the user + can tap on cells to toggle them between inactive, blank or numbered. + +4. **Verify Clue Text:** The inferred "Across" and "Down" clues are displayed, + and the user can edit them for accuracy. After this step, the app validates + that the user's edits on the grid have resulted in a consistent puzzle, e.g. + there are numbers on the grid that match the clues, etc. + +5. **LLM-based Solving:** The application uses Gemini model to solve the + puzzle. The app manages the solving loop, sending focused prompts for each + clue. The UI displays the model's confidence and color-codes letters to show + conflicts, allowing the user to watch the puzzle being solved in real-time. + + The user may pause or resume the solving process as well as start over with + a new puzzle as they choose. \ No newline at end of file diff --git a/crossword_companion/analysis_options.yaml b/crossword_companion/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/crossword_companion/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/crossword_companion/android/.gitignore b/crossword_companion/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/crossword_companion/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/crossword_companion/android/app/build.gradle.kts b/crossword_companion/android/app/build.gradle.kts new file mode 100644 index 0000000..066839f --- /dev/null +++ b/crossword_companion/android/app/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.crossword_companion" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.crossword_companion" + // 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 + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/crossword_companion/android/app/google-services.json b/crossword_companion/android/app/google-services.json new file mode 100644 index 0000000..f8111d5 --- /dev/null +++ b/crossword_companion/android/app/google-services.json @@ -0,0 +1,67 @@ +{ + "project_info": { + "project_number": "775552889844", + "project_id": "crossword-companion-b7759", + "storage_bucket": "crossword-companion-b7759.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:775552889844:android:cde9d25b42e6c936f03fa1", + "android_client_info": { + "package_name": "com.example.crossword_companion" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDU7JpCA_1eLvlYHa_Y208J3ei46KpkHx8" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:775552889844:android:45ff33e8fd39589af03fa1", + "android_client_info": { + "package_name": "com.example.flutter_crosswo" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDU7JpCA_1eLvlYHa_Y208J3ei46KpkHx8" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:775552889844:android:8ab2c0fdc70779c3f03fa1", + "android_client_info": { + "package_name": "com.example.flutter_crossword_companion" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDU7JpCA_1eLvlYHa_Y208J3ei46KpkHx8" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/crossword_companion/android/app/src/debug/AndroidManifest.xml b/crossword_companion/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/crossword_companion/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/crossword_companion/android/app/src/main/AndroidManifest.xml b/crossword_companion/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..37d06a4 --- /dev/null +++ b/crossword_companion/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crossword_companion/android/app/src/main/kotlin/com/example/crossword_companion/MainActivity.kt b/crossword_companion/android/app/src/main/kotlin/com/example/crossword_companion/MainActivity.kt new file mode 100644 index 0000000..ec33e45 --- /dev/null +++ b/crossword_companion/android/app/src/main/kotlin/com/example/crossword_companion/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.crossword_companion + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/crossword_companion/android/app/src/main/res/drawable-v21/launch_background.xml b/crossword_companion/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/crossword_companion/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/crossword_companion/android/app/src/main/res/drawable/launch_background.xml b/crossword_companion/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/crossword_companion/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/crossword_companion/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/crossword_companion/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/crossword_companion/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/crossword_companion/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/crossword_companion/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/crossword_companion/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/crossword_companion/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/crossword_companion/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/crossword_companion/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/crossword_companion/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/crossword_companion/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/crossword_companion/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/crossword_companion/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/crossword_companion/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/crossword_companion/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/crossword_companion/android/app/src/main/res/values-night/styles.xml b/crossword_companion/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/crossword_companion/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/crossword_companion/android/app/src/main/res/values/styles.xml b/crossword_companion/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/crossword_companion/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/crossword_companion/android/app/src/profile/AndroidManifest.xml b/crossword_companion/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/crossword_companion/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/crossword_companion/android/build.gradle.kts b/crossword_companion/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/crossword_companion/android/build.gradle.kts @@ -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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/crossword_companion/android/gradle.properties b/crossword_companion/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/crossword_companion/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/crossword_companion/android/gradle/wrapper/gradle-wrapper.properties b/crossword_companion/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/crossword_companion/android/gradle/wrapper/gradle-wrapper.properties @@ -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.12-all.zip diff --git a/crossword_companion/android/settings.gradle.kts b/crossword_companion/android/settings.gradle.kts new file mode 100644 index 0000000..ff284ff --- /dev/null +++ b/crossword_companion/android/settings.gradle.kts @@ -0,0 +1,29 @@ +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.9.1" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/crossword_companion/assets/cc-title.svg b/crossword_companion/assets/cc-title.svg new file mode 100644 index 0000000..c91ad57 --- /dev/null +++ b/crossword_companion/assets/cc-title.svg @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crossword_companion/assets/cc-title.svg.vec b/crossword_companion/assets/cc-title.svg.vec new file mode 100644 index 0000000..51f7b8a Binary files /dev/null and b/crossword_companion/assets/cc-title.svg.vec differ diff --git a/crossword_companion/devtools_options.yaml b/crossword_companion/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/crossword_companion/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/crossword_companion/ios/.gitignore b/crossword_companion/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/crossword_companion/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +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 diff --git a/crossword_companion/ios/Flutter/AppFrameworkInfo.plist b/crossword_companion/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/crossword_companion/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/crossword_companion/ios/Flutter/Debug.xcconfig b/crossword_companion/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/crossword_companion/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/crossword_companion/ios/Flutter/Release.xcconfig b/crossword_companion/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/crossword_companion/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/crossword_companion/ios/Podfile b/crossword_companion/ios/Podfile new file mode 100644 index 0000000..6649374 --- /dev/null +++ b/crossword_companion/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '15.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 diff --git a/crossword_companion/ios/Runner.xcodeproj/project.pbxproj b/crossword_companion/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..24df49d --- /dev/null +++ b/crossword_companion/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,732 @@ +// !$*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 */; }; + 33F1E93A641EBEF19662D5CA /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 148FB525A99CE069C75EB9DD /* GoogleService-Info.plist */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + DD24B70BA1B2F3B717DAFB8D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF9FA8C80473229BBAA9EC81 /* Pods_Runner.framework */; }; + FB8FC6480B266E596C250441 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87A3FBBBAD55B721A9BAFB8C /* Pods_RunnerTests.framework */; }; +/* 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 */ + 02631963DF074D98C4D94B61 /* 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 = ""; }; + 0B9E48F289134B8B815A06A0 /* 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 = ""; }; + 148FB525A99CE069C75EB9DD /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 488047A4CA7CF75335BC1CB9 /* 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 = ""; }; + 5DC9EA2F2AA2C12CCDA79937 /* 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 87A3FBBBAD55B721A9BAFB8C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DF9FA8C80473229BBAA9EC81 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FAA2B9F9FC65E2A60DB742E6 /* 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 = ""; }; + FF9D88EF78D7FAA4AD37DEB7 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DD24B70BA1B2F3B717DAFB8D /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A89B19E69C1759B1F05BD73E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FB8FC6480B266E596C250441 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 6EED3AB89C7B6B6E569913C4 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DF9FA8C80473229BBAA9EC81 /* Pods_Runner.framework */, + 87A3FBBBAD55B721A9BAFB8C /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 148FB525A99CE069C75EB9DD /* GoogleService-Info.plist */, + B5A79047352A064715ED3039 /* Pods */, + 6EED3AB89C7B6B6E569913C4 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + B5A79047352A064715ED3039 /* Pods */ = { + isa = PBXGroup; + children = ( + 5DC9EA2F2AA2C12CCDA79937 /* Pods-Runner.debug.xcconfig */, + FF9D88EF78D7FAA4AD37DEB7 /* Pods-Runner.release.xcconfig */, + FAA2B9F9FC65E2A60DB742E6 /* Pods-Runner.profile.xcconfig */, + 02631963DF074D98C4D94B61 /* Pods-RunnerTests.debug.xcconfig */, + 488047A4CA7CF75335BC1CB9 /* Pods-RunnerTests.release.xcconfig */, + 0B9E48F289134B8B815A06A0 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 6016E37099C15589CB841864 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + A89B19E69C1759B1F05BD73E /* 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 = ( + 044B34897A7E5A4B1954EC6C /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 9ED9CD583A81BE10B3A5029B /* [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 = ( + ); + 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 */, + 33F1E93A641EBEF19662D5CA /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 044B34897A7E5A4B1954EC6C /* [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; + }; + 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"; + }; + 6016E37099C15589CB841864 /* [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; + }; + 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"; + }; + 9ED9CD583A81BE10B3A5029B /* [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 */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 02631963DF074D98C4D94B61 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 488047A4CA7CF75335BC1CB9 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0B9E48F289134B8B815A06A0 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + 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; + "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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */; +} diff --git a/crossword_companion/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/crossword_companion/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/crossword_companion/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/crossword_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/crossword_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/crossword_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/crossword_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/crossword_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/crossword_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/crossword_companion/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/crossword_companion/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/crossword_companion/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crossword_companion/ios/Runner.xcworkspace/contents.xcworkspacedata b/crossword_companion/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/crossword_companion/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/crossword_companion/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/crossword_companion/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/crossword_companion/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/crossword_companion/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/crossword_companion/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/crossword_companion/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/crossword_companion/ios/Runner/AppDelegate.swift b/crossword_companion/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/crossword_companion/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/crossword_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/crossword_companion/ios/Runner/Base.lproj/LaunchScreen.storyboard b/crossword_companion/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/crossword_companion/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crossword_companion/ios/Runner/Base.lproj/Main.storyboard b/crossword_companion/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/crossword_companion/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crossword_companion/ios/Runner/GoogleService-Info.plist b/crossword_companion/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..337c158 --- /dev/null +++ b/crossword_companion/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyDxmtUAXptAZ4ioCP0XK8qwal__JIc_cuQ + GCM_SENDER_ID + 775552889844 + PLIST_VERSION + 1 + BUNDLE_ID + com.example.crosswordCompanion + PROJECT_ID + crossword-companion-b7759 + STORAGE_BUCKET + crossword-companion-b7759.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:775552889844:ios:7ff0fbf45b8bfd98f03fa1 + + \ No newline at end of file diff --git a/crossword_companion/ios/Runner/Info.plist b/crossword_companion/ios/Runner/Info.plist new file mode 100644 index 0000000..b19f7f1 --- /dev/null +++ b/crossword_companion/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Crossword Companion + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + crossword_companion + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSCameraUsageDescription + This app needs access to your camera to take pictures of crossword puzzles. + NSPhotoLibraryUsageDescription + This app needs access to your photo library to select images of crossword puzzles. + + diff --git a/crossword_companion/ios/Runner/Runner-Bridging-Header.h b/crossword_companion/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/crossword_companion/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/crossword_companion/ios/RunnerTests/RunnerTests.swift b/crossword_companion/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/crossword_companion/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/crossword_companion/lib/main.dart b/crossword_companion/lib/main.dart new file mode 100644 index 0000000..20f804c --- /dev/null +++ b/crossword_companion/lib/main.dart @@ -0,0 +1,55 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'firebase_options.dart'; +import 'screens/crossword_screen.dart'; +import 'services/gemini_service.dart'; +import 'state/app_step_state.dart'; +import 'state/puzzle_data_state.dart'; +import 'state/puzzle_solver_state.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + // Create instances of the services and state notifiers. + final geminiService = GeminiService(); + final appStepState = AppStepState(); + final puzzleDataState = PuzzleDataState(geminiService: geminiService); + final puzzleSolverState = PuzzleSolverState( + puzzleDataState: puzzleDataState, + geminiService: geminiService, + ); + + // Wire up the dependency between data changes and solver initialization. + puzzleDataState.onDataChanged = puzzleSolverState.initializeTodos; + + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: appStepState), + ChangeNotifierProvider.value(value: puzzleDataState), + ChangeNotifierProvider.value(value: puzzleSolverState), + ], + child: MaterialApp( + theme: ThemeData.from( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + home: const CrosswordScreen(), + debugShowCheckedModeBanner: false, + ), + ); + } +} diff --git a/crossword_companion/lib/models/clue.dart b/crossword_companion/lib/models/clue.dart new file mode 100644 index 0000000..7ec05a1 --- /dev/null +++ b/crossword_companion/lib/models/clue.dart @@ -0,0 +1,44 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:uuid/uuid.dart'; + +enum ClueDirection { across, down } + +class Clue { + Clue({required this.number, required this.direction, required this.text}) + : id = const Uuid().v4(); + + Clue.private({ + required this.id, + required this.number, + required this.direction, + required this.text, + }); + + factory Clue.fromJson(Map json) => Clue( + number: json['number'], + direction: ClueDirection.values.byName(json['direction']), + text: json['text'], + ); + String id; + int number; + ClueDirection direction; + String text; + + Clue copyWith({int? number, ClueDirection? direction, String? text}) => + Clue.private( + id: id, + number: number ?? this.number, + direction: direction ?? this.direction, + text: text ?? this.text, + ); + + Map toJson() => { + 'id': id, + 'number': number, + 'direction': direction.toString().split('.').last, + 'text': text, + }; +} diff --git a/crossword_companion/lib/models/clue_answer.dart b/crossword_companion/lib/models/clue_answer.dart new file mode 100644 index 0000000..cf21873 --- /dev/null +++ b/crossword_companion/lib/models/clue_answer.dart @@ -0,0 +1,14 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +class ClueAnswer { + ClueAnswer({required this.answer, required this.confidence}); + final String answer; + final double confidence; + + ClueAnswer copyWith({String? answer, double? confidence}) => ClueAnswer( + answer: answer ?? this.answer, + confidence: confidence ?? this.confidence, + ); +} diff --git a/crossword_companion/lib/models/crossword_data.dart b/crossword_companion/lib/models/crossword_data.dart new file mode 100644 index 0000000..b121e4a --- /dev/null +++ b/crossword_companion/lib/models/crossword_data.dart @@ -0,0 +1,47 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'clue.dart'; +import 'crossword_grid.dart'; + +class CrosswordData { + CrosswordData({ + required this.width, + required this.height, + required this.grid, + required this.clues, + }); + + factory CrosswordData.fromJson(Map json) => CrosswordData( + width: json['width'], + height: json['height'], + grid: CrosswordGrid.fromJson(json['grid']), + clues: (json['clues'] as List) + .map((clueJson) => Clue.fromJson(clueJson)) + .toList(), + ); + final int width; + final int height; + final CrosswordGrid grid; + final List clues; + + Map toJson() => { + 'width': width, + 'height': height, + 'grid': grid.toJson(), + 'clues': clues.map((clue) => clue.toJson()).toList(), + }; + + CrosswordData copyWith({ + int? width, + int? height, + CrosswordGrid? grid, + List? clues, + }) => CrosswordData( + width: width ?? this.width, + height: height ?? this.height, + grid: grid ?? this.grid, + clues: clues ?? this.clues, + ); +} diff --git a/crossword_companion/lib/models/crossword_grid.dart b/crossword_companion/lib/models/crossword_grid.dart new file mode 100644 index 0000000..5246953 --- /dev/null +++ b/crossword_companion/lib/models/crossword_grid.dart @@ -0,0 +1,37 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'grid_cell.dart'; + +class CrosswordGrid { + CrosswordGrid({ + required this.width, + required this.height, + required this.cells, + }); + + factory CrosswordGrid.fromJson(Map json) => CrosswordGrid( + width: json['width'], + height: json['height'], + cells: (json['cells'] as List) + .map((cellJson) => GridCell.fromJson(cellJson)) + .toList(), + ); + final int width; + final int height; + final List cells; + + Map toJson() => { + 'width': width, + 'height': height, + 'cells': cells.map((cell) => cell.toJson()).toList(), + }; + + CrosswordGrid copyWith({int? width, int? height, List? cells}) => + CrosswordGrid( + width: width ?? this.width, + height: height ?? this.height, + cells: cells ?? this.cells, + ); +} diff --git a/crossword_companion/lib/models/grid_cell.dart b/crossword_companion/lib/models/grid_cell.dart new file mode 100644 index 0000000..ff4c7f1 --- /dev/null +++ b/crossword_companion/lib/models/grid_cell.dart @@ -0,0 +1,68 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +enum GridCellType { inactive, empty, numbered } + +class GridCell { + GridCell({ + this.type = GridCellType.empty, + this.clueNumber, + this.acrossLetter, + this.downLetter, + this.userLetter, + }); + + factory GridCell.fromJson(Map json) { + final typeString = json['type'] as String?; + GridCellType type; + switch (typeString) { + case 'inactive': + type = GridCellType.inactive; + case 'numbered': + type = GridCellType.numbered; + case 'empty': + default: + type = GridCellType.empty; + } + + return GridCell( + type: type, + clueNumber: json['clueNumber'] as int?, + acrossLetter: json['acrossLetter'] as String?, + downLetter: json['downLetter'] as String?, + userLetter: json['userLetter'] as String?, + ); + } + final GridCellType type; + final int? clueNumber; + final String? acrossLetter; + final String? downLetter; + final String? userLetter; + + Map toJson() => { + 'type': type.toString().split('.').last, + 'clueNumber': clueNumber, + 'acrossLetter': acrossLetter, + 'downLetter': downLetter, + 'userLetter': userLetter, + }; + + GridCell copyWith({ + GridCellType? type, + int? clueNumber, + bool clearClueNumber = false, + String? acrossLetter, + bool clearAcrossLetter = false, + String? downLetter, + bool clearDownLetter = false, + String? userLetter, + bool clearUserLetter = false, + }) => GridCell( + type: type ?? this.type, + clueNumber: clearClueNumber ? null : clueNumber ?? this.clueNumber, + acrossLetter: clearAcrossLetter ? null : acrossLetter ?? this.acrossLetter, + downLetter: clearDownLetter ? null : downLetter ?? this.downLetter, + userLetter: clearUserLetter ? null : userLetter ?? this.userLetter, + ); +} diff --git a/crossword_companion/lib/models/todo_item.dart b/crossword_companion/lib/models/todo_item.dart new file mode 100644 index 0000000..0b70ab5 --- /dev/null +++ b/crossword_companion/lib/models/todo_item.dart @@ -0,0 +1,22 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'clue_answer.dart'; + +enum TodoStatus { notDone, inProgress, done } + +class TodoItem { + TodoItem({ + required this.id, + required this.description, + this.status = TodoStatus.notDone, + this.answer, + this.isWrong = false, + }); + final String id; + final String description; + final TodoStatus status; + final ClueAnswer? answer; + final bool isWrong; +} diff --git a/crossword_companion/lib/platform/platform.dart b/crossword_companion/lib/platform/platform.dart new file mode 100644 index 0000000..ea8fa91 --- /dev/null +++ b/crossword_companion/lib/platform/platform.dart @@ -0,0 +1,5 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'platform_web.dart' if (dart.library.io) 'platform_io.dart'; diff --git a/crossword_companion/lib/platform/platform_io.dart b/crossword_companion/lib/platform/platform_io.dart new file mode 100644 index 0000000..9efae90 --- /dev/null +++ b/crossword_companion/lib/platform/platform_io.dart @@ -0,0 +1,8 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +bool isMobile() => Platform.isIOS || Platform.isAndroid; +bool isDesktop() => Platform.isWindows || Platform.isMacOS || Platform.isLinux; diff --git a/crossword_companion/lib/platform/platform_web.dart b/crossword_companion/lib/platform/platform_web.dart new file mode 100644 index 0000000..436a2b0 --- /dev/null +++ b/crossword_companion/lib/platform/platform_web.dart @@ -0,0 +1,6 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +bool isMobile() => false; +bool isDesktop() => true; diff --git a/crossword_companion/lib/screens/crossword_screen.dart b/crossword_companion/lib/screens/crossword_screen.dart new file mode 100644 index 0000000..e545022 --- /dev/null +++ b/crossword_companion/lib/screens/crossword_screen.dart @@ -0,0 +1,104 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:provider/provider.dart'; +import 'package:vector_graphics/vector_graphics.dart'; + +import '../state/app_step_state.dart'; +import '../widgets/step1_select_image.dart'; +import '../widgets/step2_verify_grid_size.dart'; +import '../widgets/step3_verify_grid_contents.dart'; +import '../widgets/step4_verify_clue_text.dart'; +import '../widgets/step5_solve_puzzle.dart'; + +const _showScreenWidth = false; + +class CrosswordScreen extends StatelessWidget { + const CrosswordScreen({super.key}); + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, appStepState, child) { + final steps = [ + Step( + title: const Text('Select crossword image'), + content: StepOneSelectImage(isActive: appStepState.currentStep == 0), + ), + Step( + title: const Text('Verify grid size'), + content: StepTwoVerifyGridSize( + isActive: appStepState.currentStep == 1, + ), + ), + Step( + title: const Text('Verify grid contents'), + content: StepThreeVerifyGridContents( + isActive: appStepState.currentStep == 2, + ), + ), + Step( + title: const Text('Verify grid clues'), + content: StepFourVerifyClueText( + isActive: appStepState.currentStep == 3, + ), + ), + Step( + title: const Text('Solve the puzzle'), + content: StepFiveSolvePuzzle(isActive: appStepState.currentStep == 4), + ), + ]; + + return Scaffold( + body: Column( + children: [ + const Padding( + padding: EdgeInsets.only(left: 32, right: 32, top: 64), + child: SvgPicture( + AssetBytesLoader('assets/cc-title.svg.vec'), + height: 100, + ), + ), + Expanded( + child: Stepper( + currentStep: appStepState.currentStep, + onStepTapped: null, + onStepContinue: null, + onStepCancel: null, + // Hide the default buttons + controlsBuilder: (_, _) => const SizedBox.shrink(), + steps: steps.asMap().entries.map((entry) { + final index = entry.key; + final step = entry.value; + return Step( + title: step.title, + content: step.content, + state: appStepState.currentStep > index + ? StepState.complete + : StepState.indexed, + isActive: appStepState.currentStep == index, + ); + }).toList(), + ), + ), + if (_showScreenWidth) + Container( + color: Colors.grey[200], + padding: const EdgeInsets.all(8), + child: LayoutBuilder( + builder: (context, constraints) => Center( + child: Text( + 'Screen width: ' + '${constraints.maxWidth.toStringAsFixed(0)}px', + ), + ), + ), + ), + ], + ), + ); + }, + ); +} diff --git a/crossword_companion/lib/services/gemini_service.dart b/crossword_companion/lib/services/gemini_service.dart new file mode 100644 index 0000000..3c591c0 --- /dev/null +++ b/crossword_companion/lib/services/gemini_service.dart @@ -0,0 +1,417 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_dynamic_calls + +import 'dart:async'; +import 'dart:convert'; + +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; + +import '../models/clue.dart'; +import '../models/clue_answer.dart'; +import '../models/crossword_data.dart'; +import '../models/crossword_grid.dart'; +import '../models/grid_cell.dart'; + +class GeminiService { + GeminiService() { + // The model for inferring crossword data from images. + _crosswordModel = FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-pro', + generationConfig: GenerationConfig( + responseMimeType: 'application/json', + responseSchema: _crosswordSchema, + ), + ); + + // The model for solving clues. + _clueSolverModel = FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash', + systemInstruction: Content.text(clueSolverSystemInstruction), + tools: [ + Tool.functionDeclarations([ + _getWordMetadataFunction, + _returnResultFunction, + _resolveConflictFunction, + ]), + ], + ); + } + + late final GenerativeModel _crosswordModel; + late final GenerativeModel _clueSolverModel; + StreamSubscription? _clueSolverSubscription; + + Future cancelCurrentSolve() async { + await _clueSolverSubscription?.cancel(); + _clueSolverSubscription = null; + } + + static final _getWordMetadataFunction = FunctionDeclaration( + 'getWordMetadata', + 'Gets grammatical metadata for a word, like its part of speech. ' + 'Best used to verify a candidate answer against a clue that implies a ' + 'grammatical constraint.', + parameters: { + 'word': Schema(SchemaType.string, description: 'The word to look up.'), + }, + ); + + static final _returnResultFunction = FunctionDeclaration( + 'returnResult', + 'Returns the final result of the clue solving process.', + parameters: { + 'answer': Schema( + SchemaType.string, + description: 'The answer to the clue.', + ), + 'confidence': Schema( + SchemaType.number, + description: 'The confidence score in the answer from 0.0 to 1.0.', + ), + }, + ); + + static final _resolveConflictFunction = FunctionDeclaration( + 'resolveConflict', + 'Asks the user to resolve a conflict between the letter pattern and the ' + 'proposed answer. Use this BEFORE calling returnResult if the answer you ' + 'want to propose does not match the letter pattern.', + parameters: { + 'proposedAnswer': Schema( + SchemaType.string, + description: 'The answer the LLM wants to suggest.', + ), + 'pattern': Schema( + SchemaType.string, + description: 'The current letter pattern from the grid.', + ), + 'clue': Schema(SchemaType.string, description: 'The clue text.'), + }, + ); + + static String get clueSolverSystemInstruction => + ''' +You are an expert crossword puzzle solver. + +**Follow these rules at all times:** +1. **Prefer Common Words:** Prioritize common English words and proper nouns. Avoid obscure, archaic, or highly technical terms unless the clue strongly implies them. +2. **Match the Clue:** Ensure your answer strictly matches the clue's tense, plurality (singular vs. plural), and part of speech. +3. **Verify Grammatically:** If a clue implies a specific part of speech (e.g., it's a verb, adverb, or plural), it's a good idea to use the `getWordMetadata` tool to verify your candidate answer matches. However, avoid using it for every clue. +4. **Be Confident:** Provide a confidence score from 0.0 to 1.0 indicating your certainty. +5. **Trust the Clue Over the Pattern:** The provided letter pattern is only a suggestion based on other potentially incorrect answers. Your primary goal is to find the best word that fits the **clue text**. +6. **Resolve Conflicts:** If the answer you are confident in conflicts with the provided `pattern`, you **MUST** use the `resolveConflict` tool to ask the user for the correct answer. Use the result of `resolveConflict` as your final answer. +7. **Format Correctly:** You must return your answer in the specified JSON format. + +--- + +### Tool: `getWordMetadata` + +You have a tool to get grammatical information about a word. + +**When to use:** +- This tool is most helpful as a verification step after you have a likely answer. +- Consider using this tool when a clue contains a grammatical hint that could be ambiguous. +- **Good candidates for verification:** +- Clues that seem to be verbs (e.g., "To run," "Waving"). +- Clues that are adverbs (e.g., "Happily," "Quickly"). +- Clues that specify a plural form. +- **Try to avoid using the tool for:** +- Simple definitions (e.g., "A small dog"). +- Fill-in-the-blank clues (e.g., "___ and flow"). +- Proper nouns (e.g., "Capital of France"). + +**Function signature:** +```json +${jsonEncode(_getWordMetadataFunction.toJson())} +``` + +### Tool: `returnResult` + +You have a tool to return the final result of the clue solving process. + +**When to use:** +- Use this tool when you have a final answer and confidence score to return. You +must use this tool exactly once, and only once, to return the final result. + +**Function signature:** +```json +${jsonEncode(_returnResultFunction.toJson())} +``` + +### Tool: `resolveConflict` + +You have a tool to ask the user to resolve a conflict. + +**When to use:** +- Use this tool **BEFORE** `returnResult` if your proposed answer conflicts with the provided letter pattern. +- For example, if the pattern is `_ R _ Y` and you want to suggest `RENT` (which fits the clue), there is a conflict at the second letter (`R` vs `E`). You should call `resolveConflict(proposedAnswer: "RENT", pattern: "_ R _ Y", clue: "...")`. +- The tool will return the user's decision (either your proposed answer or a new one). You should then use that result to call `returnResult`. + +**Function signature:** +```json +${jsonEncode(_resolveConflictFunction.toJson())} +``` +'''; + + static final _crosswordSchema = Schema( + SchemaType.object, + properties: { + 'width': Schema(SchemaType.integer), + 'height': Schema(SchemaType.integer), + 'grid': Schema( + SchemaType.array, + items: Schema( + SchemaType.array, + items: Schema( + SchemaType.object, + properties: { + 'color': Schema(SchemaType.string), + 'clueNumber': Schema(SchemaType.integer, nullable: true), + }, + ), + ), + ), + 'clues': Schema( + SchemaType.object, + properties: { + 'across': Schema( + SchemaType.array, + items: Schema( + SchemaType.object, + properties: { + 'number': Schema(SchemaType.integer), + 'text': Schema(SchemaType.string), + }, + ), + ), + 'down': Schema( + SchemaType.array, + items: Schema( + SchemaType.object, + properties: { + 'number': Schema(SchemaType.integer), + 'text': Schema(SchemaType.string), + }, + ), + ), + }, + ), + }, + ); + + Future inferCrosswordData(List images) async { + final imageParts = []; + for (final image in images) { + final imageBytes = await image.readAsBytes(); + final mimeType = lookupMimeType(image.path, headerBytes: imageBytes)!; + imageParts.add(InlineDataPart(mimeType, imageBytes)); + } + + final content = [ + Content.multi([ + TextPart(''' +Analyze the following crossword puzzle images and return a JSON object +representing the grid size, contents, and clues. The images may contain +different parts of the same puzzle (e.g., the grid the across clues, the down +clues). Combine them to form a complete puzzle. +The JSON schema is as follows: ${jsonEncode(_crosswordSchema.toJson())} + '''), + ...imageParts, + ]), + ]; + + final response = await _crosswordModel.generateContent(content); + + final json = jsonDecode(response.text!); + + final width = json['width'] as int; + final height = json['height'] as int; + final gridData = json['grid'] as List; + final cluesData = json['clues'] as Map; + + final cells = gridData + .expand( + (row) => (row as List).map((cellData) { + final isBlack = cellData['color'] == 'black'; + final type = isBlack ? GridCellType.inactive : GridCellType.empty; + final clueNumber = isBlack ? null : cellData['clueNumber'] as int?; + return GridCell(type: type, clueNumber: clueNumber); + }), + ) + .toList(); + + final grid = CrosswordGrid(width: width, height: height, cells: cells); + + final acrossClues = (cluesData['across'] as List).map( + (clueData) => Clue( + number: clueData['number'], + direction: ClueDirection.across, + text: clueData['text'], + ), + ); + + final downClues = (cluesData['down'] as List).map( + (clueData) => Clue( + number: clueData['number'], + direction: ClueDirection.down, + text: clueData['text'], + ), + ); + + final clues = [...acrossClues, ...downClues]; + + return CrosswordData( + width: width, + height: height, + grid: grid, + clues: clues, + ); + } + + // Buffer for the result of the clue solving process. + final _returnResult = {}; + + Future solveClue( + Clue clue, + int length, + String pattern, { + Future Function(String clue, String proposedAnswer, String pattern)? + onConflict, + }) async { + // Cancel any previous, in-flight request. + await cancelCurrentSolve(); + + // Clear the return result cache; this is where the result will be stored. + _returnResult.clear(); + + // Generate JSON response with functions and schema. + await _clueSolverModel.generateContentWithFunctions( + prompt: getSolverPrompt(clue, length, pattern), + onFunctionCall: (functionCall) async => switch (functionCall.name) { + 'getWordMetadata' => await _getWordMetadataFromApi( + functionCall.args['word'] as String, + ), + 'returnResult' => _cacheReturnResult(functionCall.args), + 'resolveConflict' => await _handleResolveConflict( + functionCall.args, + onConflict, + ), + _ => throw Exception('Unknown function call: ${functionCall.name}'), + }, + ); + + assert(_returnResult.isNotEmpty, 'The return result cache is empty.'); + return ClueAnswer( + answer: _returnResult['answer'] as String, + confidence: (_returnResult['confidence'] as num).toDouble(), + ); + } + + // Look up the metadata for a word in the dictionary API. + Future> _getWordMetadataFromApi(String word) async { + debugPrint('Looking up metadata for word: "$word"'); + final url = Uri.parse( + 'https://api.dictionaryapi.dev/api/v2/entries/en/${Uri.encodeComponent(word)}', + ); + + final response = await http.get(url); + return response.statusCode == 200 + ? {'result': jsonDecode(response.body)} + : {'error': 'Could not find a definition for "$word".'}; + } + + // Cache the return result of the clue solving process via a function call. + // This is how we get JSON responses from the model with functions, since the + // model cannot return JSON directly when tools are used. + Map _cacheReturnResult(Map returnResult) { + debugPrint('Caching return result: ${jsonEncode(returnResult)}'); + assert(_returnResult.isEmpty, 'The return result cache is not empty.'); + _returnResult.addAll(returnResult); + return {'status': 'success'}; + } + + Future> _handleResolveConflict( + Map args, + Future Function(String clue, String proposedAnswer, String pattern)? + onConflict, + ) async { + final proposedAnswer = args['proposedAnswer'] as String; + final pattern = args['pattern'] as String; + final clue = args['clue'] as String; + + if (onConflict != null) { + final result = await onConflict(clue, proposedAnswer, pattern); + return {'result': result}; + } + + return {'result': proposedAnswer}; + } + + String getSolverPrompt(Clue clue, int length, String pattern) => + ''' +Your task is to solve the following crossword clue. + +**Clue:** "${clue.text}" + +**Constraints:** +- The answer is a **$length-letter** word. +- The current letter pattern is `$pattern`, where `_` represents an unknown letter. + +Return your answer and confidence score in the required JSON format. +'''; +} + +extension on GenerativeModel { + Future generateContentWithFunctions({ + required String prompt, + required Future> Function(FunctionCall) onFunctionCall, + }) async { + // Use a chat session to support multiple request/response pairs, which is + // needed to support function calls. + final chat = startChat(); + final buffer = StringBuffer(); + var response = await chat.sendMessage(Content.text(prompt)); + + while (true) { + // Append the response text to the buffer. + buffer.write(response.text ?? ''); + + // If no function calls were collected, we're done + if (response.functionCalls.isEmpty) break; + + // Append a newline to separate responses. + buffer.write('\n'); + + // Execute all function calls + final functionResponses = []; + for (final functionCall in response.functionCalls) { + try { + functionResponses.add( + FunctionResponse( + functionCall.name, + await onFunctionCall(functionCall), + ), + ); + } catch (ex) { + functionResponses.add( + FunctionResponse(functionCall.name, {'error': ex.toString()}), + ); + } + } + + // Get the next response stream with function results + response = await chat.sendMessage( + Content.functionResponses(functionResponses), + ); + } + + return buffer.toString(); + } +} diff --git a/crossword_companion/lib/services/image_picker_service.dart b/crossword_companion/lib/services/image_picker_service.dart new file mode 100644 index 0000000..ec62648 --- /dev/null +++ b/crossword_companion/lib/services/image_picker_service.dart @@ -0,0 +1,19 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:image_picker/image_picker.dart'; + +class ImagePickerService { + ImagePickerService({ImagePicker? picker}) : _picker = picker ?? ImagePicker(); + final ImagePicker _picker; + + Future pickImageFromGallery() async => + _picker.pickImage(source: ImageSource.gallery); + + Future pickImageFromCamera() async => + _picker.pickImage(source: ImageSource.camera); + + Future> pickMultipleImagesFromGallery() async => + _picker.pickMultiImage(); +} diff --git a/crossword_companion/lib/services/puzzle_solver.dart b/crossword_companion/lib/services/puzzle_solver.dart new file mode 100644 index 0000000..c4574a8 --- /dev/null +++ b/crossword_companion/lib/services/puzzle_solver.dart @@ -0,0 +1,167 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../models/clue.dart'; +import '../models/grid_cell.dart'; +import '../models/todo_item.dart'; +import '../state/puzzle_data_state.dart'; +import '../state/puzzle_solver_state.dart'; +import 'gemini_service.dart'; + +class PuzzleSolver { + Future solve( + PuzzleSolverState solverState, + PuzzleDataState dataState, + GeminiService geminiService, { + bool isResuming = false, + Future Function(String clue, String proposedAnswer, String pattern)? + onConflict, + }) async { + assert( + solverState.todos.isNotEmpty, + 'The list of todos should not be empty when calling solvePuzzle', + ); + if (!isResuming) { + assert(dataState.isGridClear); + } + if (dataState.crosswordData == null) return; + + solverState.isSolving = true; + + while (solverState.isSolving && + solverState.todos.any((todo) => todo.status != TodoStatus.done)) { + for (final todo in solverState.todos) { + if (!solverState.isSolving) break; + + if (todo.status == TodoStatus.done) continue; + + solverState.updateTodoStatus(todo.id, TodoStatus.inProgress); + + final clue = dataState.crosswordData!.clues.firstWhere( + (clue) => clue.id == todo.id, + ); + + final expectedLength = _getClueLength(dataState, clue); + final pattern = _getCluePattern(dataState, clue); + + final clueAnswer = await geminiService.solveClue( + clue, + expectedLength, + pattern, + onConflict: onConflict, + ); + + if (!solverState.isSolving) break; + + if (clueAnswer != null) { + if (clueAnswer.answer.length == expectedLength) { + final conflicts = _getConflicts(dataState, clue, clueAnswer.answer); + if (conflicts.isEmpty) { + dataState.setSolution( + clue.number, + clue.direction, + clueAnswer.answer, + ); + solverState.updateTodoStatus( + todo.id, + TodoStatus.done, + answer: clueAnswer, + ); + } else { + solverState.updateTodoStatus( + todo.id, + TodoStatus.notDone, + answer: clueAnswer.copyWith( + answer: '-- CONFLICTS WITH YOUR LETTER', + ), + isWrong: true, + ); + } + } else { + solverState.updateTodoStatus( + todo.id, + TodoStatus.notDone, + answer: clueAnswer, + isWrong: true, + ); + } + } else { + solverState.updateTodoStatus(todo.id, TodoStatus.notDone); + } + } + } + + solverState.isSolving = false; + } + + // Helper method to execute a callback for each cell in a clue. + void _traverseClue( + PuzzleDataState dataState, + Clue clue, + void Function(GridCell) onCell, + ) { + final grid = dataState.crosswordData!.grid; + final startIndex = grid.cells.indexWhere( + (c) => c.clueNumber == clue.number, + ); + if (startIndex == -1) return; + + if (clue.direction == ClueDirection.across) { + final startRow = startIndex ~/ grid.width; + for (var i = startIndex; i < grid.cells.length; i++) { + final currentRow = i ~/ grid.width; + if (currentRow != startRow || + grid.cells[i].type == GridCellType.inactive) { + break; + } + onCell(grid.cells[i]); + } + } else { + for (var i = startIndex; i < grid.cells.length; i += grid.width) { + if (grid.cells[i].type == GridCellType.inactive) { + break; + } + onCell(grid.cells[i]); + } + } + } + + int _getClueLength(PuzzleDataState dataState, Clue clue) { + var length = 0; + _traverseClue(dataState, clue, (cell) { + if (cell.type != GridCellType.inactive) { + length++; + } + }); + return length; + } + + String _getCluePattern(PuzzleDataState dataState, Clue clue) { + var pattern = ''; + _traverseClue(dataState, clue, (cell) { + if (cell.userLetter != null) { + pattern += cell.userLetter!; + } else if (clue.direction == ClueDirection.across) { + pattern += cell.downLetter ?? '_'; + } else { + pattern += cell.acrossLetter ?? '_'; + } + }); + return pattern; + } + + List _getConflicts(PuzzleDataState dataState, Clue clue, String answer) { + final conflicts = []; + var i = 0; + _traverseClue(dataState, clue, (cell) { + if (cell.userLetter != null && + i < answer.length && + cell.userLetter != answer[i]) { + conflicts.add(i); + } + i++; + }); + return conflicts; + } +} diff --git a/crossword_companion/lib/state/app_step_state.dart b/crossword_companion/lib/state/app_step_state.dart new file mode 100644 index 0000000..628accd --- /dev/null +++ b/crossword_companion/lib/state/app_step_state.dart @@ -0,0 +1,29 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +class AppStepState with ChangeNotifier { + int _currentStep = 0; + int get currentStep => _currentStep; + + void nextStep() { + if (_currentStep < 4) { + _currentStep++; + notifyListeners(); + } + } + + void previousStep() { + if (_currentStep > 0) { + _currentStep--; + notifyListeners(); + } + } + + void reset() { + _currentStep = 0; + notifyListeners(); + } +} diff --git a/crossword_companion/lib/state/puzzle_data_state.dart b/crossword_companion/lib/state/puzzle_data_state.dart new file mode 100644 index 0000000..f38af0a --- /dev/null +++ b/crossword_companion/lib/state/puzzle_data_state.dart @@ -0,0 +1,276 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../models/clue.dart'; +import '../models/crossword_data.dart'; +import '../models/grid_cell.dart'; +import '../services/gemini_service.dart'; + +class PuzzleDataState with ChangeNotifier { + PuzzleDataState({required GeminiService geminiService}) + : _geminiService = geminiService; + + final GeminiService _geminiService; + + VoidCallback? onDataChanged; + + final List _selectedCrosswordImages = []; + List get selectedCrosswordImages => _selectedCrosswordImages; + + final List _selectedCrosswordImagesData = []; + List get selectedCrosswordImagesData => + _selectedCrosswordImagesData; + + CrosswordData? _crosswordData; + CrosswordData? get crosswordData => _crosswordData; + + bool get isGridClear => + _crosswordData?.grid.cells.every( + (cell) => cell.acrossLetter == null && cell.downLetter == null, + ) ?? + true; + + void updateCrosswordData(CrosswordData? newData) { + if (newData != null && + _crosswordData != null && + (newData.width != _crosswordData!.width || + newData.height != _crosswordData!.height)) { + final oldGrid = _crosswordData!.grid; + final newCells = []; + + for (var y = 0; y < newData.height; y++) { + for (var x = 0; x < newData.width; x++) { + if (x < oldGrid.width && y < oldGrid.height) { + // This cell existed in the old grid, so copy it. + final oldIndex = y * oldGrid.width + x; + newCells.add(oldGrid.cells[oldIndex]); + } else { + // This is a new cell, create a default one. + newCells.add(GridCell()); + } + } + } + + final newGrid = _crosswordData!.grid.copyWith( + width: newData.width, + height: newData.height, + cells: newCells, + ); + _crosswordData = newData.copyWith(grid: newGrid); + } else { + _crosswordData = newData; + } + onDataChanged?.call(); + notifyListeners(); + } + + List validateGridAndClues() { + if (_crosswordData == null) return []; + + final errors = []; + + // Rule 1: Unique Grid Numbers + final gridNumbers = _crosswordData!.grid.cells + .where((c) => c.clueNumber != null) + .map((c) => c.clueNumber!) + .toList(); + final uniqueGridNumbers = gridNumbers.toSet(); + if (gridNumbers.length != uniqueGridNumbers.length) { + final duplicates = gridNumbers + .fold>({}, (map, n) { + map[n] = (map[n] ?? 0) + 1; + return map; + }) + .entries + .where((e) => e.value > 1) + .map((e) => e.key) + .toList(); + for (final duplicate in duplicates) { + errors.add('Duplicate number in grid: #$duplicate'); + } + } + + // Rule 2 & 3: Parity between clues and grid numbers + final clueNumbers = _crosswordData!.clues.map((c) => c.number).toSet(); + + final cluesWithoutGridEntry = clueNumbers.difference(uniqueGridNumbers); + for (final number in cluesWithoutGridEntry) { + final missingClues = _crosswordData!.clues.where( + (c) => c.number == number, + ); + for (final clue in missingClues) { + errors.add( + "Clue '${clue.number} ${clue.direction.name}' exists, " + 'but #${clue.number} is not in the grid.', + ); + } + } + + final gridEntriesWithoutClue = uniqueGridNumbers.difference(clueNumbers); + for (final number in gridEntriesWithoutClue) { + errors.add('Grid contains #$number, but there is no clue for it.'); + } + + return errors; + } + + void updateClue(Clue updatedClue) { + if (_crosswordData == null) return; + + final newClues = List.from(_crosswordData!.clues); + final index = newClues.indexWhere((c) => c.id == updatedClue.id); + if (index != -1) { + newClues[index] = updatedClue; + _crosswordData = _crosswordData!.copyWith(clues: newClues); + onDataChanged?.call(); + notifyListeners(); + } + } + + bool _isInferringCrosswordData = false; + bool get isInferringCrosswordData => _isInferringCrosswordData; + + String? _inferenceError; + String? get inferenceError => _inferenceError; + + Future inferCrosswordData() async { + if (_selectedCrosswordImages.isEmpty) { + return; + } + + _isInferringCrosswordData = true; + _inferenceError = null; + notifyListeners(); + + try { + _crosswordData = await _geminiService.inferCrosswordData( + _selectedCrosswordImages, + ); + onDataChanged?.call(); + } on Exception catch (e) { + _inferenceError = e.toString(); + } + + _isInferringCrosswordData = false; + notifyListeners(); + } + + Future addSelectedCrosswordImages(List images) async { + _selectedCrosswordImages.addAll(images); + _crosswordData = null; // Clear old crossword data + + for (final image in images) { + _selectedCrosswordImagesData.add(await image.readAsBytes()); + } + onDataChanged?.call(); + notifyListeners(); + } + + void removeSelectedCrosswordImage(int index) { + _selectedCrosswordImages.removeAt(index); + _selectedCrosswordImagesData.removeAt(index); + _crosswordData = null; // Clear old crossword data + onDataChanged?.call(); + notifyListeners(); + } + + void setCellType( + int index, + GridCellType type, { + int? clueNumber, + bool clearClueNumber = false, + }) { + if (_crosswordData != null) { + final newCells = List.from(_crosswordData!.grid.cells); + final oldCell = newCells[index]; + + newCells[index] = GridCell( + type: type, + clueNumber: clearClueNumber ? null : clueNumber ?? oldCell.clueNumber, + acrossLetter: oldCell.acrossLetter, + downLetter: oldCell.downLetter, + userLetter: oldCell.userLetter, + ); + + final newGrid = _crosswordData!.grid.copyWith(cells: newCells); + _crosswordData = _crosswordData!.copyWith(grid: newGrid); + notifyListeners(); + } + } + + void updateCellLetter(int index, String letter) { + if (_crosswordData != null) { + final newCells = List.from(_crosswordData!.grid.cells); + final oldCell = newCells[index]; + + if (oldCell.type == GridCellType.inactive) return; + + if (letter.isEmpty) { + newCells[index] = oldCell.copyWith( + clearUserLetter: true, + clearAcrossLetter: true, + clearDownLetter: true, + ); + } else { + newCells[index] = oldCell.copyWith( + userLetter: letter.toUpperCase(), + clearAcrossLetter: true, + clearDownLetter: true, + ); + } + + final newGrid = _crosswordData!.grid.copyWith(cells: newCells); + _crosswordData = _crosswordData!.copyWith(grid: newGrid); + notifyListeners(); + } + } + + void setSolution(int clueNumber, ClueDirection direction, String? answer) { + if (_crosswordData == null) return; + + final newCells = List.from(_crosswordData!.grid.cells); + final startIndex = newCells.indexWhere( + (cell) => cell.clueNumber == clueNumber, + ); + + if (startIndex != -1 && answer != null) { + for (var i = 0; i < answer.length; i++) { + int cellIndex; + if (direction == ClueDirection.across) { + cellIndex = startIndex + i; + } else { + cellIndex = startIndex + (i * _crosswordData!.width); + } + + if (cellIndex < newCells.length && + newCells[cellIndex].type != GridCellType.inactive && + newCells[cellIndex].userLetter == null) { + final oldCell = newCells[cellIndex]; + newCells[cellIndex] = direction == ClueDirection.across + ? oldCell.copyWith(acrossLetter: answer[i].toUpperCase()) + : oldCell.copyWith(downLetter: answer[i].toUpperCase()); + } + } + } + + final newGrid = _crosswordData!.grid.copyWith(cells: newCells); + _crosswordData = _crosswordData!.copyWith(grid: newGrid); + + notifyListeners(); + } + + void reset() { + _selectedCrosswordImages.clear(); + _selectedCrosswordImagesData.clear(); + _crosswordData = null; + _isInferringCrosswordData = false; + _inferenceError = null; + notifyListeners(); + } +} diff --git a/crossword_companion/lib/state/puzzle_solver_state.dart b/crossword_companion/lib/state/puzzle_solver_state.dart new file mode 100644 index 0000000..334f262 --- /dev/null +++ b/crossword_companion/lib/state/puzzle_solver_state.dart @@ -0,0 +1,164 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import '../models/clue.dart'; +import '../models/clue_answer.dart'; +import '../models/grid_cell.dart'; +import '../models/todo_item.dart'; +import '../services/gemini_service.dart'; +import '../services/puzzle_solver.dart'; +import 'puzzle_data_state.dart'; + +class PuzzleSolverState with ChangeNotifier { + PuzzleSolverState({ + required PuzzleDataState puzzleDataState, + required GeminiService geminiService, + }) // + : _puzzleDataState = puzzleDataState, + _geminiService = geminiService; + + final PuzzleDataState _puzzleDataState; + final GeminiService _geminiService; + final _puzzleSolver = PuzzleSolver(); + + List _todos = []; + List get todos => _todos; + + void setTodos(List newTodos) { + _todos = newTodos; + notifyListeners(); + } + + void updateTodoStatus( + String todoId, + TodoStatus newStatus, { + ClueAnswer? answer, + bool isWrong = false, + }) { + final index = _todos.indexWhere((todo) => todo.id == todoId); + if (index != -1) { + _todos[index] = TodoItem( + id: _todos[index].id, + description: _todos[index].description, + status: newStatus, + answer: answer ?? _todos[index].answer, + isWrong: isWrong, + ); + notifyListeners(); + } + } + + bool _isSolving = false; + bool get isSolving => _isSolving; + set isSolving(bool value) { + _isSolving = value; + notifyListeners(); + } + + Future pauseSolving() async { + isSolving = false; + await _geminiService.cancelCurrentSolve(); + } + + Future resumeSolving() => solvePuzzle(isResuming: true); + + Future restartSolving() async { + if (_puzzleDataState.crosswordData == null) return; + + // Stop any existing solving loops and invalidate their results. + await pauseSolving(); + + // Clear AI-generated letters from the grid, preserving user-entered letters + final newCells = _puzzleDataState.crosswordData!.grid.cells + .map( + (cell) => + cell.copyWith(clearAcrossLetter: true, clearDownLetter: true), + ) + .toList(); + final newGrid = _puzzleDataState.crosswordData!.grid.copyWith( + cells: newCells, + ); + _puzzleDataState.updateCrosswordData( + _puzzleDataState.crosswordData!.copyWith(grid: newGrid), + ); + + // Reset todos + initializeTodos(); + + // Start solving + unawaited(solvePuzzle()); + } + + Future solvePuzzle({ + bool isResuming = false, + Future Function(String clue, String proposedAnswer, String pattern)? + onConflict, + }) => _puzzleSolver.solve( + this, + _puzzleDataState, + _geminiService, + isResuming: isResuming, + onConflict: onConflict, + ); + + void resetSolution() { + if (_puzzleDataState.crosswordData == null) return; + + // Clear letters from the grid by creating new cells + final newCells = _puzzleDataState.crosswordData!.grid.cells + .map( + (cell) => GridCell( + type: cell.type, + clueNumber: cell.clueNumber, + acrossLetter: null, + downLetter: null, + userLetter: null, + ), + ) + .toList(); + final newGrid = _puzzleDataState.crosswordData!.grid.copyWith( + cells: newCells, + ); + _puzzleDataState.updateCrosswordData( + _puzzleDataState.crosswordData!.copyWith(grid: newGrid), + ); + + // Reset todos + initializeTodos(); + + notifyListeners(); + } + + void initializeTodos() { + if (_puzzleDataState.crosswordData == null) { + _todos = []; + notifyListeners(); + return; + } + + final newTodos = _puzzleDataState.crosswordData!.clues + .map( + (clue) => TodoItem( + id: clue.id, + description: + '${clue.number}' + '${clue.direction == ClueDirection.across ? 'A' : 'D'}. ' + '${clue.text}', + ), + ) + .toList(); + + setTodos(newTodos); + } + + void reset() { + _todos = []; + _isSolving = false; + notifyListeners(); + } +} diff --git a/crossword_companion/lib/styles.dart b/crossword_companion/lib/styles.dart new file mode 100644 index 0000000..255d7dc --- /dev/null +++ b/crossword_companion/lib/styles.dart @@ -0,0 +1,29 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +const Color conflictColor = Colors.red; +const Color matchingColor = Colors.green; +const Color defaultLetterColor = Colors.black87; +const Color userLetterColor = Colors.blue; +const Color inactiveCellColor = Color(0xFFBDBDBD); // A light grey +const Color emptyCellColor = Colors.white; +const Color cellBorderColor = Colors.grey; + +const TextStyle clueNumberStyle = TextStyle(fontSize: 8); +const TextStyle letterStyle = TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, +); + +final ButtonStyle secondaryActionButtonStyle = ElevatedButton.styleFrom( + disabledBackgroundColor: Colors.transparent, + disabledForegroundColor: const Color.fromRGBO(0, 0, 0, 0.7), +); + +final ButtonStyle primaryActionButtonStyle = ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, +); diff --git a/crossword_companion/lib/widgets/clue_list.dart b/crossword_companion/lib/widgets/clue_list.dart new file mode 100644 index 0000000..23990f2 --- /dev/null +++ b/crossword_companion/lib/widgets/clue_list.dart @@ -0,0 +1,124 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../models/clue.dart'; + +class ClueList extends StatelessWidget { + const ClueList({required this.clues, required this.onClueUpdated, super.key}); + final List clues; + final Function(Clue) onClueUpdated; + + @override + Widget build(BuildContext context) { + final acrossClues = clues + .where((c) => c.direction == ClueDirection.across) + .toList(); + final downClues = clues + .where((c) => c.direction == ClueDirection.down) + .toList(); + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Across', style: Theme.of(context).textTheme.headlineSmall), + ...acrossClues.map((c) => _buildClueItem(context, c)), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Down', style: Theme.of(context).textTheme.headlineSmall), + ...downClues.map((c) => _buildClueItem(context, c)), + ], + ), + ), + ], + ); + } + + Widget _buildClueItem(BuildContext context, Clue clue) => InkWell( + onTap: () => _editClue(context, clue), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text('${clue.number}. ${clue.text}'), + ), + ); + + void _editClue(BuildContext context, Clue clue) { + final textController = TextEditingController(text: clue.text); + final numberController = TextEditingController( + text: clue.number.toString(), + ); + final focusNode = FocusNode(); + + unawaited( + showDialog( + context: context, + builder: (context) => KeyboardListener( + focusNode: focusNode, + onKeyEvent: (event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.escape) { + Navigator.of(context).pop(); + } else if (event.logicalKey == LogicalKeyboardKey.enter) { + final newClue = clue.copyWith( + text: textController.text, + number: int.tryParse(numberController.text) ?? clue.number, + ); + onClueUpdated(newClue); + Navigator.of(context).pop(); + } + } + }, + child: AlertDialog( + title: Text('Edit Clue ${clue.number} ${clue.direction.name}'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: numberController, + decoration: const InputDecoration(labelText: 'Clue Number'), + keyboardType: TextInputType.number, + autofocus: true, + ), + TextField( + controller: textController, + decoration: const InputDecoration(labelText: 'Clue Text'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + final newClue = clue.copyWith( + text: textController.text, + number: int.tryParse(numberController.text) ?? clue.number, + ); + onClueUpdated(newClue); + Navigator.of(context).pop(); + }, + child: const Text('Save'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/crossword_companion/lib/widgets/conflict_dialog.dart b/crossword_companion/lib/widgets/conflict_dialog.dart new file mode 100644 index 0000000..189a38e2 --- /dev/null +++ b/crossword_companion/lib/widgets/conflict_dialog.dart @@ -0,0 +1,77 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +class ConflictDialog extends StatefulWidget { + const ConflictDialog({ + required this.clue, + required this.pattern, + required this.proposedAnswer, + super.key, + }); + + final String clue; + final String pattern; + final String proposedAnswer; + + @override + State createState() => _ConflictDialogState(); +} + +class _ConflictDialogState extends State { + late final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.proposedAnswer); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Conflict Detected'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Clue: ${widget.clue}'), + const SizedBox(height: 8), + Text('Pattern: ${widget.pattern}'), + const SizedBox(height: 16), + TextField( + controller: _controller, + decoration: const InputDecoration( + labelText: 'Answer', + border: OutlineInputBorder(), + ), + autofocus: true, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + // Return the original proposed answer if canceled + Navigator.pop(context, widget.proposedAnswer); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context, _controller.text); + }, + child: const Text('OK'), + ), + ], + ); + } +} diff --git a/crossword_companion/lib/widgets/grid_view.dart b/crossword_companion/lib/widgets/grid_view.dart new file mode 100644 index 0000000..ccd6019 --- /dev/null +++ b/crossword_companion/lib/widgets/grid_view.dart @@ -0,0 +1,101 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../models/crossword_grid.dart'; +import '../models/grid_cell.dart'; +import '../styles.dart'; + +class CrosswordGridView extends StatefulWidget { + const CrosswordGridView({ + required this.grid, + required this.onCellTapped, + super.key, + }); + final CrosswordGrid grid; + final Function(int) onCellTapped; + + @override + State createState() => _CrosswordGridViewState(); +} + +class _CrosswordGridViewState extends State { + int _hoveredIndex = -1; + + @override + Widget build(BuildContext context) => GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.grid.width, + ), + itemCount: widget.grid.cells.length, + itemBuilder: (context, index) { + final cell = widget.grid.cells[index]; + final letter = cell.userLetter ?? cell.acrossLetter ?? cell.downLetter; + final hasConflict = + cell.acrossLetter != null && + cell.downLetter != null && + cell.acrossLetter != cell.downLetter; + final hasMatch = + cell.acrossLetter != null && + cell.downLetter != null && + cell.acrossLetter == cell.downLetter; + + final letterColor = cell.userLetter != null + ? userLetterColor + : hasConflict + ? conflictColor + : hasMatch + ? matchingColor + : defaultLetterColor; + + var cellColor = cell.type == GridCellType.inactive + ? Colors.black + : emptyCellColor; + + if (_hoveredIndex == index) { + cellColor = Color.alphaBlend( + const Color.fromRGBO(0, 0, 0, 0.2), + cellColor, + ); + } + + return MouseRegion( + onEnter: (_) => setState(() => _hoveredIndex = index), + onExit: (_) => setState(() => _hoveredIndex = -1), + child: GestureDetector( + onTap: () => widget.onCellTapped(index), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: cellBorderColor), + color: cellColor, + ), + child: Stack( + children: [ + if (cell.clueNumber != null) + Positioned( + top: 2, + left: 2, + child: Text( + cell.clueNumber.toString(), + style: clueNumberStyle, + ), + ), + if (letter != null) + Center( + child: Text( + letter, + style: letterStyle.copyWith(color: letterColor), + ), + ), + ], + ), + ), + ), + ); + }, + ); +} diff --git a/crossword_companion/lib/widgets/selected_images_view.dart b/crossword_companion/lib/widgets/selected_images_view.dart new file mode 100644 index 0000000..b855ecd --- /dev/null +++ b/crossword_companion/lib/widgets/selected_images_view.dart @@ -0,0 +1,46 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; + +class SelectedImagesView extends StatelessWidget { + const SelectedImagesView({ + required this.imagesData, + super.key, + this.onRemoveImage, + }); + final List imagesData; + final Function(int)? onRemoveImage; + + @override + Widget build(BuildContext context) => SizedBox( + height: 500, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: imagesData.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only(right: 8), + child: Stack( + children: [ + Image.memory(imagesData[index], fit: BoxFit.contain), + if (onRemoveImage != null) + Positioned( + top: 0, + right: 0, + child: IconButton( + icon: const Icon( + Icons.remove_circle_outline, + color: Colors.red, + ), + onPressed: () => onRemoveImage!(index), + ), + ), + ], + ), + ), + ), + ); +} diff --git a/crossword_companion/lib/widgets/step1_select_image.dart b/crossword_companion/lib/widgets/step1_select_image.dart new file mode 100644 index 0000000..e35d62f --- /dev/null +++ b/crossword_companion/lib/widgets/step1_select_image.dart @@ -0,0 +1,98 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../platform/platform.dart'; +import '../services/image_picker_service.dart'; +import '../state/app_step_state.dart'; +import '../state/puzzle_data_state.dart'; +import '../styles.dart'; +import 'selected_images_view.dart'; +import 'step_activation_mixin.dart'; + +class StepOneSelectImage extends StatefulWidget { + const StepOneSelectImage({required this.isActive, super.key}); + + final bool isActive; + + @override + State createState() => _StepOneSelectImageState(); +} + +class _StepOneSelectImageState extends State + with StepActivationMixin { + final _imagePickerService = ImagePickerService(); + + @override + bool get isActive => widget.isActive; + + @override + void onActivated() { + final puzzleDataState = Provider.of( + context, + listen: false, + ); + assert(puzzleDataState.isGridClear); + } + + @override + Widget build(BuildContext context) { + final puzzleDataState = Provider.of(context); + final appStepState = Provider.of(context); + final areImagesSelected = + puzzleDataState.selectedCrosswordImages.isNotEmpty; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (areImagesSelected) + SelectedImagesView( + imagesData: puzzleDataState.selectedCrosswordImagesData, + onRemoveImage: puzzleDataState.removeSelectedCrosswordImage, + ), + const SizedBox(height: 16), + Wrap( + alignment: WrapAlignment.start, + spacing: 8, + runSpacing: 8, + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.photo_library), + label: const Text('Gallery'), + onPressed: () async { + final images = await _imagePickerService + .pickMultipleImagesFromGallery(); + await puzzleDataState.addSelectedCrosswordImages(images); + }, + style: secondaryActionButtonStyle, + ), + ElevatedButton.icon( + icon: const Icon(Icons.camera_alt), + label: const Text('Photo'), + onPressed: isMobile() + ? () async { + final image = await _imagePickerService + .pickImageFromCamera(); + if (image != null) { + await puzzleDataState.addSelectedCrosswordImages([ + image, + ]); + } + } + : null, + style: secondaryActionButtonStyle, + ), + ElevatedButton( + onPressed: areImagesSelected ? appStepState.nextStep : null, + style: primaryActionButtonStyle, + child: const Text('Next'), + ), + ], + ), + ], + ); + } +} diff --git a/crossword_companion/lib/widgets/step2_verify_grid_size.dart b/crossword_companion/lib/widgets/step2_verify_grid_size.dart new file mode 100644 index 0000000..299e7ca --- /dev/null +++ b/crossword_companion/lib/widgets/step2_verify_grid_size.dart @@ -0,0 +1,248 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../state/app_step_state.dart'; +import '../state/puzzle_data_state.dart'; +import '../styles.dart'; +import 'selected_images_view.dart'; +import 'step_activation_mixin.dart'; + +class StepTwoVerifyGridSize extends StatefulWidget { + const StepTwoVerifyGridSize({required this.isActive, super.key}); + + final bool isActive; + + @override + State createState() => _StepTwoVerifyGridSizeState(); +} + +class _StepTwoVerifyGridSizeState extends State + with StepActivationMixin { + int? _newWidth; + int? _newHeight; + + @override + bool get isActive => widget.isActive; + + @override + void onActivated() { + final puzzleDataState = Provider.of( + context, + listen: false, + ); + assert(puzzleDataState.isGridClear); + + if (puzzleDataState.selectedCrosswordImages.isNotEmpty && + puzzleDataState.crosswordData == null && + !puzzleDataState.isInferringCrosswordData && + puzzleDataState.inferenceError == null) { + unawaited(puzzleDataState.inferCrosswordData()); + } + } + + @override + Widget build(BuildContext context) { + final puzzleDataState = Provider.of(context); + final appStepState = Provider.of(context); + + if (puzzleDataState.isInferringCrosswordData) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 16), + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Inferring crossword data...'), + SizedBox(height: 8), + Text('(This could take a couple of minutes)'), + ], + ), + ); + } + + if (puzzleDataState.inferenceError != null) { + return Column( + children: [ + Text( + 'Error: ${puzzleDataState.inferenceError}\n' + 'Please go back and try again.', + style: const TextStyle(color: Colors.red), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: appStepState.previousStep, + child: const Text('Back'), + ), + ], + ), + ], + ); + } + + if (puzzleDataState.crosswordData == null) { + // This can happen if the inference fails. + return Column( + children: [ + const Text( + 'Could not infer crossword data. Please go back and try again.', + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: appStepState.previousStep, + child: const Text('Back'), + ), + ], + ), + ], + ); + } + + final crosswordData = puzzleDataState.crosswordData!; + _newWidth ??= crosswordData.width; + _newHeight ??= crosswordData.height; + + assert(puzzleDataState.selectedCrosswordImagesData.isNotEmpty); + + return Column( + children: [ + SelectedImagesView( + imagesData: puzzleDataState.selectedCrosswordImagesData, + ), + const SizedBox(height: 24), + Align( + alignment: Alignment.centerLeft, + child: Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 32, + runSpacing: 32, + children: [ + // Rows Stepper + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + _buildCircularChevronButton( + icon: Icons.keyboard_arrow_up, + onPressed: () { + setState(() { + _newHeight = (_newHeight ?? 0) + 1; + }); + }, + ), + const SizedBox(height: 16), + _buildCircularChevronButton( + icon: Icons.keyboard_arrow_down, + onPressed: () { + if ((_newHeight ?? 0) > 1) { + setState(() { + _newHeight = (_newHeight ?? 0) - 1; + }); + } + }, + ), + ], + ), + const SizedBox(width: 8), + Text( + '${_newHeight ?? 0} Rows', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + // Columns Stepper + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildCircularChevronButton( + icon: Icons.keyboard_arrow_left, + onPressed: () { + if ((_newWidth ?? 0) > 1) { + setState(() { + _newWidth = (_newWidth ?? 0) - 1; + }); + } + }, + ), + const SizedBox(width: 8), + Text( + '${_newWidth ?? 0} Columns', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(width: 8), + _buildCircularChevronButton( + icon: Icons.keyboard_arrow_right, + onPressed: () { + setState(() { + _newWidth = (_newWidth ?? 0) + 1; + }); + }, + ), + ], + ), + ], + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: appStepState.previousStep, + style: secondaryActionButtonStyle, + child: const Text('Back'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: puzzleDataState.crosswordData != null + ? () { + final currentData = puzzleDataState.crosswordData!; + puzzleDataState.updateCrosswordData( + currentData.copyWith( + width: _newWidth ?? currentData.width, + height: _newHeight ?? currentData.height, + ), + ); + + appStepState.nextStep(); + } + : null, + style: primaryActionButtonStyle, + child: const Text('Next'), + ), + ], + ), + ], + ); + } + + Widget _buildCircularChevronButton({ + required IconData icon, + required VoidCallback onPressed, + }) => OutlinedButton( + onPressed: onPressed, + style: OutlinedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(8), + side: const BorderSide(color: Colors.black), + backgroundColor: Colors.transparent, + ), + child: Icon(icon, color: Colors.black), + ); +} diff --git a/crossword_companion/lib/widgets/step3_verify_grid_contents.dart b/crossword_companion/lib/widgets/step3_verify_grid_contents.dart new file mode 100644 index 0000000..dbd1cec --- /dev/null +++ b/crossword_companion/lib/widgets/step3_verify_grid_contents.dart @@ -0,0 +1,226 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import '../models/grid_cell.dart'; +import '../state/app_step_state.dart'; +import '../state/puzzle_data_state.dart'; +import '../styles.dart'; +import 'grid_view.dart'; +import 'selected_images_view.dart'; +import 'step_activation_mixin.dart'; + +class StepThreeVerifyGridContents extends StatefulWidget { + const StepThreeVerifyGridContents({required this.isActive, super.key}); + + final bool isActive; + + @override + State createState() => + _StepThreeVerifyGridContentsState(); +} + +class _StepThreeVerifyGridContentsState + extends State + with StepActivationMixin { + @override + bool get isActive => widget.isActive; + + @override + void onActivated() { + final puzzleDataState = Provider.of( + context, + listen: false, + ); + assert(puzzleDataState.isGridClear); + } + + void _showEditCellDialog(BuildContext context, int index) { + final puzzleDataState = Provider.of( + context, + listen: false, + ); + unawaited( + showDialog( + context: context, + builder: (context) => SimpleDialog( + title: const Text('Edit Cell'), + children: [ + SimpleDialogOption( + onPressed: () { + puzzleDataState.setCellType( + index, + GridCellType.empty, + clearClueNumber: true, + ); + Navigator.pop(context); + }, + child: const Text('Empty (white)'), + ), + SimpleDialogOption( + onPressed: () { + puzzleDataState.setCellType( + index, + GridCellType.inactive, + clearClueNumber: true, + ); + Navigator.pop(context); + }, + child: const Text('Inactive (black)'), + ), + SimpleDialogOption( + onPressed: () { + Navigator.pop(context); + _showEnterNumberDialog(context, index); + }, + child: const Text('Numbered'), + ), + ], + ), + ), + ); + } + + void _showEnterNumberDialog(BuildContext context, int index) { + final puzzleDataState = Provider.of( + context, + listen: false, + ); + final controller = TextEditingController(); + final errorNotifier = ValueNotifier(null); + final focusNode = FocusNode(); + + unawaited( + showDialog( + context: context, + builder: (context) => KeyboardListener( + focusNode: focusNode, + onKeyEvent: (event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.escape) { + Navigator.pop(context); + } else if (event.logicalKey == LogicalKeyboardKey.enter) { + final number = int.tryParse(controller.text); + if (number != null) { + puzzleDataState.setCellType( + index, + GridCellType.empty, + clueNumber: number, + ); + Navigator.pop(context); + } + } + } + }, + child: ValueListenableBuilder( + valueListenable: errorNotifier, + builder: (context, errorText, child) => AlertDialog( + title: const Text('Enter Number'), + content: TextField( + controller: controller, + keyboardType: TextInputType.number, + autofocus: true, + decoration: InputDecoration(errorText: errorText), + onChanged: (value) { + if (int.tryParse(value) == null) { + errorNotifier.value = 'Invalid number'; + } else { + errorNotifier.value = null; + } + }, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: errorText == null + ? () { + final number = int.tryParse(controller.text); + if (number != null) { + puzzleDataState.setCellType( + index, + GridCellType.empty, + clueNumber: number, + ); + Navigator.pop(context); + } + } + : null, + child: const Text('OK'), + ), + ], + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) => + Consumer2( + builder: (context, puzzleDataState, appStepState, child) { + if (puzzleDataState.crosswordData == null) { + return const SizedBox.shrink(); + } + assert(puzzleDataState.selectedCrosswordImagesData.isNotEmpty); + return Column( + children: [ + SelectedImagesView( + imagesData: puzzleDataState.selectedCrosswordImagesData, + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: CrosswordGridView( + key: ValueKey(puzzleDataState.crosswordData!.grid), + grid: puzzleDataState.crosswordData!.grid, + onCellTapped: (index) { + _showEditCellDialog(context, index); + }, + ), + ), + Text( + 'Tap a cell to correct its contents.', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: appStepState.previousStep, + style: secondaryActionButtonStyle, + child: const Text('Back'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: appStepState.nextStep, + style: primaryActionButtonStyle, + child: const Text('Next'), + ), + ], + ), + ], + ); + }, + ); +} diff --git a/crossword_companion/lib/widgets/step4_verify_clue_text.dart b/crossword_companion/lib/widgets/step4_verify_clue_text.dart new file mode 100644 index 0000000..3e9ed97 --- /dev/null +++ b/crossword_companion/lib/widgets/step4_verify_clue_text.dart @@ -0,0 +1,115 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../state/app_step_state.dart'; +import '../state/puzzle_data_state.dart'; +import '../styles.dart'; +import 'clue_list.dart'; +import 'selected_images_view.dart'; + +class StepFourVerifyClueText extends StatefulWidget { + const StepFourVerifyClueText({required this.isActive, super.key}); + + final bool isActive; + + @override + State createState() => _StepFourVerifyClueTextState(); +} + +class _StepFourVerifyClueTextState extends State { + @override + Widget build(BuildContext context) { + final puzzleDataState = Provider.of(context); + final appStepState = Provider.of(context); + final areCluesSet = + puzzleDataState.crosswordData != null && + puzzleDataState.crosswordData!.clues.isNotEmpty; + + if (puzzleDataState.crosswordData == null) { + return const Center(child: CircularProgressIndicator()); + } + + final crosswordData = puzzleDataState.crosswordData!; + assert(puzzleDataState.selectedCrosswordImagesData.isNotEmpty); + + return SingleChildScrollView( + child: Column( + children: [ + SelectedImagesView( + imagesData: puzzleDataState.selectedCrosswordImagesData, + ), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ClueList( + clues: crosswordData.clues, + onClueUpdated: puzzleDataState.updateClue, + ), + const SizedBox(height: 8), + Text( + 'Tap a clue to edit its text.', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: appStepState.previousStep, + style: secondaryActionButtonStyle, + child: const Text('Back'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: areCluesSet + ? () { + final errors = puzzleDataState.validateGridAndClues(); + if (errors.isNotEmpty) { + unawaited( + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Validation Errors'), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: errors + .map((e) => Text('- $e')) + .toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => + Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ), + ); + } else { + appStepState.nextStep(); + } + } + : null, + style: primaryActionButtonStyle, + child: const Text('Solve'), + ), + ], + ), + ], + ), + ); + } +} diff --git a/crossword_companion/lib/widgets/step5_solve_puzzle.dart b/crossword_companion/lib/widgets/step5_solve_puzzle.dart new file mode 100644 index 0000000..12d8d8c --- /dev/null +++ b/crossword_companion/lib/widgets/step5_solve_puzzle.dart @@ -0,0 +1,246 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import '../models/todo_item.dart'; +import '../state/app_step_state.dart'; +import '../state/puzzle_data_state.dart'; +import '../state/puzzle_solver_state.dart'; +import '../styles.dart'; +import 'conflict_dialog.dart'; +import 'grid_view.dart'; +import 'step_activation_mixin.dart'; +import 'todo_list_widget.dart'; + +class StepFiveSolvePuzzle extends StatefulWidget { + const StepFiveSolvePuzzle({required this.isActive, super.key}); + + final bool isActive; + + @override + State createState() => _StepFiveSolvePuzzleState(); +} + +class _StepFiveSolvePuzzleState extends State + with StepActivationMixin { + @override + bool get isActive => widget.isActive; + + @override + void onActivated() { + final puzzleSolverState = Provider.of( + context, + listen: false, + ); + + // Start solving only if we are not already solving and there are todos. + if (!puzzleSolverState.isSolving && + puzzleSolverState.todos.any((t) => t.status != TodoStatus.done)) { + unawaited( + puzzleSolverState.solvePuzzle( + onConflict: (clue, proposedAnswer, pattern) => + _showConflictDialog(context, clue, proposedAnswer, pattern), + ), + ); + } + } + + void _showEditLetterDialog(BuildContext context, int index) { + final puzzleDataState = Provider.of( + context, + listen: false, + ); + final controller = TextEditingController(); + final focusNode = FocusNode(); + + unawaited( + showDialog( + context: context, + builder: (context) => KeyboardListener( + focusNode: focusNode, + onKeyEvent: (event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.escape) { + Navigator.pop(context); + } else if (event.logicalKey == LogicalKeyboardKey.enter) { + puzzleDataState.updateCellLetter(index, controller.text); + Navigator.pop(context); + } + } + }, + child: AlertDialog( + title: const Text('Letter'), + content: TextField( + controller: controller, + autofocus: true, + maxLength: 1, + ), + actions: [ + TextButton( + onPressed: () { + puzzleDataState.updateCellLetter(index, ''); + Navigator.pop(context); + }, + child: const Text('Delete'), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + puzzleDataState.updateCellLetter(index, controller.text); + Navigator.pop(context); + }, + child: const Text('OK'), + ), + ], + ), + ), + ), + ); + } + + Future _showConflictDialog( + BuildContext context, + String clue, + String proposedAnswer, + String pattern, + ) async { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => ConflictDialog( + clue: clue, + pattern: pattern, + proposedAnswer: proposedAnswer, + ), + ); + return result ?? proposedAnswer; + } + + @override + Widget build(BuildContext context) { + final puzzleDataState = Provider.of(context); + final puzzleSolverState = Provider.of(context); + final appStepState = Provider.of(context); + + if (puzzleDataState.crosswordData == null) { + return const Center(child: CircularProgressIndicator()); + } + + final crosswordData = puzzleDataState.crosswordData!; + return SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Align( + alignment: Alignment.centerLeft, + child: Wrap( + alignment: WrapAlignment.end, + spacing: 8, + runSpacing: 8, + children: [ + if (puzzleSolverState.isSolving) + ElevatedButton( + onPressed: puzzleSolverState.pauseSolving, + child: const Text('Pause'), + ), + if (!puzzleSolverState.isSolving && + puzzleSolverState.todos.any( + (t) => t.status != TodoStatus.done, + )) + ElevatedButton( + onPressed: () => puzzleSolverState.resumeSolving(), + child: const Text('Resume'), + ), + ElevatedButton( + onPressed: puzzleSolverState.restartSolving, + child: const Text('Restart'), + ), + ElevatedButton( + onPressed: () async { + if (puzzleSolverState.isSolving) { + await puzzleSolverState.pauseSolving(); + } + puzzleSolverState.resetSolution(); + appStepState.previousStep(); + }, + style: secondaryActionButtonStyle, + child: const Text('Back'), + ), + ElevatedButton( + onPressed: () { + appStepState.reset(); + puzzleDataState.reset(); + puzzleSolverState.reset(); + }, + style: primaryActionButtonStyle, + child: const Text('New Puzzle'), + ), + ], + ), + ), + ), + LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 600) { + // Narrow screen: stack grid and clues vertically + return Column( + children: [ + CrosswordGridView( + grid: crosswordData.grid, + onCellTapped: (index) => + _showEditLetterDialog(context, index), + ), + const SizedBox(height: 16), + TodoListWidget(todos: puzzleSolverState.todos), + ], + ); + } else { + // Wide screen: display grid and clues side-by-side + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: CrosswordGridView( + grid: crosswordData.grid, + onCellTapped: (index) => + _showEditLetterDialog(context, index), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TodoListWidget(todos: puzzleSolverState.todos), + ), + ], + ); + } + }, + ), + if (puzzleSolverState.isSolving) + const Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Solving puzzle...'), + ], + ), + ), + ], + ), + ); + } +} diff --git a/crossword_companion/lib/widgets/step_activation_mixin.dart b/crossword_companion/lib/widgets/step_activation_mixin.dart new file mode 100644 index 0000000..cb5ef23 --- /dev/null +++ b/crossword_companion/lib/widgets/step_activation_mixin.dart @@ -0,0 +1,28 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +mixin StepActivationMixin on State { + bool get isActive; + + @override + void initState() { + super.initState(); + if (isActive) { + onActivated(); + } + } + + @override + void didUpdateWidget(covariant T oldWidget) { + super.didUpdateWidget(oldWidget); + final oldIsActive = (oldWidget as dynamic).isActive as bool; + if (!oldIsActive && isActive) { + WidgetsBinding.instance.addPostFrameCallback((_) => onActivated()); + } + } + + void onActivated(); +} diff --git a/crossword_companion/lib/widgets/todo_list_widget.dart b/crossword_companion/lib/widgets/todo_list_widget.dart new file mode 100644 index 0000000..3608096 --- /dev/null +++ b/crossword_companion/lib/widgets/todo_list_widget.dart @@ -0,0 +1,63 @@ +// Copyright 2025 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../models/todo_item.dart'; +import '../styles.dart'; + +class TodoListWidget extends StatelessWidget { + const TodoListWidget({required this.todos, super.key}); + final List todos; + + @override + Widget build(BuildContext context) => ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: todos.length, + itemBuilder: (context, index) { + final todo = todos[index]; + final confidence = todo.answer?.confidence ?? 0; + final confidenceString = '(${(confidence * 100).toStringAsFixed(0)}%)'; + + return ListTile( + title: RichText( + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: todo.description, + style: TextStyle(color: _getColorForStatus(todo.status)), + ), + if (todo.answer != null) + TextSpan( + text: ': ${todo.answer!.answer} $confidenceString', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + if (todo.isWrong) + const TextSpan( + text: ' -- WRONG', + style: TextStyle( + fontWeight: FontWeight.bold, + color: conflictColor, + ), + ), + ], + ), + ), + ); + }, + ); + + Color _getColorForStatus(TodoStatus status) { + switch (status) { + case TodoStatus.done: + return matchingColor; + case TodoStatus.inProgress: + return defaultLetterColor; + case TodoStatus.notDone: + return conflictColor; + } + } +} diff --git a/crossword_companion/macos/.gitignore b/crossword_companion/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/crossword_companion/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/crossword_companion/macos/Flutter/Flutter-Debug.xcconfig b/crossword_companion/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/crossword_companion/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/crossword_companion/macos/Flutter/Flutter-Release.xcconfig b/crossword_companion/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/crossword_companion/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/crossword_companion/macos/Flutter/GeneratedPluginRegistrant.swift b/crossword_companion/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..4fbd8c2 --- /dev/null +++ b/crossword_companion/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_selector_macos +import firebase_app_check +import firebase_auth +import firebase_core +import path_provider_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) +} diff --git a/crossword_companion/macos/Podfile b/crossword_companion/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/crossword_companion/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_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_macos_build_settings(target) + end +end diff --git a/crossword_companion/macos/Runner.xcodeproj/project.pbxproj b/crossword_companion/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6181cbe --- /dev/null +++ b/crossword_companion/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,805 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 2C5552BFA515EAE7C23BBD09 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2BB8EB15CF17CFFF62FEF9F3 /* Pods_Runner.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 7BD8C50F6CBF75375DEDE1C2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 873B2301191E89B74460B42F /* Pods_RunnerTests.framework */; }; + A4922017ABFC851FCDC34A72 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5F8A2216EFD242B3581546A4 /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 29834DDA4A2E7DDBCA35D5C9 /* 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 = ""; }; + 2BB8EB15CF17CFFF62FEF9F3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* crossword_companion.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = crossword_companion.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 3C8FE71684FD386253F3866B /* 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 = ""; }; + 51CF10A9077FEF3BBCCC9127 /* 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 = ""; }; + 5F8A2216EFD242B3581546A4 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + 72D139987B07946E5B6ED957 /* 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 = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 873B2301191E89B74460B42F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 87E6AA3AEEE54C3582E36CB5 /* 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 = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + C3F9775AD62EC69D97094235 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BD8C50F6CBF75375DEDE1C2 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2C5552BFA515EAE7C23BBD09 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2E851B3DC04C96B2A4CCCB7C /* Pods */ = { + isa = PBXGroup; + children = ( + 72D139987B07946E5B6ED957 /* Pods-Runner.debug.xcconfig */, + 87E6AA3AEEE54C3582E36CB5 /* Pods-Runner.release.xcconfig */, + 51CF10A9077FEF3BBCCC9127 /* Pods-Runner.profile.xcconfig */, + C3F9775AD62EC69D97094235 /* Pods-RunnerTests.debug.xcconfig */, + 3C8FE71684FD386253F3866B /* Pods-RunnerTests.release.xcconfig */, + 29834DDA4A2E7DDBCA35D5C9 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 2E851B3DC04C96B2A4CCCB7C /* Pods */, + 5F8A2216EFD242B3581546A4 /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* crossword_companion.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2BB8EB15CF17CFFF62FEF9F3 /* Pods_Runner.framework */, + 873B2301191E89B74460B42F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 3F80C34AF8E73196404FC3B9 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C873AA7B66D3F8DA39685847 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + B84CE4AB6650FEF00A67B379 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* crossword_companion.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + A4922017ABFC851FCDC34A72 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 3F80C34AF8E73196404FC3B9 /* [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; + }; + B84CE4AB6650FEF00A67B379 /* [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; + }; + C873AA7B66D3F8DA39685847 /* [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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C3F9775AD62EC69D97094235 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/crossword_companion.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/crossword_companion"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3C8FE71684FD386253F3866B /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/crossword_companion.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/crossword_companion"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 29834DDA4A2E7DDBCA35D5C9 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/crossword_companion.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/crossword_companion"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/crossword_companion/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/crossword_companion/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/crossword_companion/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/crossword_companion/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/crossword_companion/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..b7e33a8 --- /dev/null +++ b/crossword_companion/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crossword_companion/macos/Runner.xcworkspace/contents.xcworkspacedata b/crossword_companion/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/crossword_companion/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/crossword_companion/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/crossword_companion/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/crossword_companion/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/crossword_companion/macos/Runner/AppDelegate.swift b/crossword_companion/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/crossword_companion/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/crossword_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/crossword_companion/macos/Runner/Base.lproj/MainMenu.xib b/crossword_companion/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/crossword_companion/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crossword_companion/macos/Runner/Configs/AppInfo.xcconfig b/crossword_companion/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..d132d0f --- /dev/null +++ b/crossword_companion/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = crossword_companion + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.crosswordCompanion + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/crossword_companion/macos/Runner/Configs/Debug.xcconfig b/crossword_companion/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/crossword_companion/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/crossword_companion/macos/Runner/Configs/Release.xcconfig b/crossword_companion/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/crossword_companion/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/crossword_companion/macos/Runner/Configs/Warnings.xcconfig b/crossword_companion/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/crossword_companion/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/crossword_companion/macos/Runner/DebugProfile.entitlements b/crossword_companion/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..8165abf --- /dev/null +++ b/crossword_companion/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-only + + + diff --git a/crossword_companion/macos/Runner/GoogleService-Info.plist b/crossword_companion/macos/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..337c158 --- /dev/null +++ b/crossword_companion/macos/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyDxmtUAXptAZ4ioCP0XK8qwal__JIc_cuQ + GCM_SENDER_ID + 775552889844 + PLIST_VERSION + 1 + BUNDLE_ID + com.example.crosswordCompanion + PROJECT_ID + crossword-companion-b7759 + STORAGE_BUCKET + crossword-companion-b7759.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:775552889844:ios:7ff0fbf45b8bfd98f03fa1 + + \ No newline at end of file diff --git a/crossword_companion/macos/Runner/Info.plist b/crossword_companion/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/crossword_companion/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/crossword_companion/macos/Runner/MainFlutterWindow.swift b/crossword_companion/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/crossword_companion/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/crossword_companion/macos/Runner/Release.entitlements b/crossword_companion/macos/Runner/Release.entitlements new file mode 100644 index 0000000..741903e --- /dev/null +++ b/crossword_companion/macos/Runner/Release.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-only + + + diff --git a/crossword_companion/macos/RunnerTests/RunnerTests.swift b/crossword_companion/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/crossword_companion/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/crossword_companion/pubspec.yaml b/crossword_companion/pubspec.yaml new file mode 100644 index 0000000..e50792c --- /dev/null +++ b/crossword_companion/pubspec.yaml @@ -0,0 +1,31 @@ +name: crossword_companion +description: "A new Flutter project." +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ^3.8.1 + +dependencies: + firebase_ai: ^3.0.0 + firebase_core: ^4.0.0 + flutter: + sdk: flutter + flutter_svg: ^2.2.1 + google_fonts: ^6.3.2 + image_picker: ^1.1.2 + path: ^1.9.0 + provider: ^6.1.5 + uuid: ^4.4.0 + vector_graphics: ^1.1.0 + http: ^1.2.1 + +dev_dependencies: + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/cc-title.svg.vec # compiled SVG diff --git a/crossword_companion/readme/screen-recording.mov b/crossword_companion/readme/screen-recording.mov new file mode 100644 index 0000000..1182d6f Binary files /dev/null and b/crossword_companion/readme/screen-recording.mov differ diff --git a/crossword_companion/sample_puzzle_images/test1.png b/crossword_companion/sample_puzzle_images/test1.png new file mode 100644 index 0000000..2a4d732 Binary files /dev/null and b/crossword_companion/sample_puzzle_images/test1.png differ diff --git a/crossword_companion/sample_puzzle_images/test2a.png b/crossword_companion/sample_puzzle_images/test2a.png new file mode 100644 index 0000000..c243a7a Binary files /dev/null and b/crossword_companion/sample_puzzle_images/test2a.png differ diff --git a/crossword_companion/sample_puzzle_images/test2b.png b/crossword_companion/sample_puzzle_images/test2b.png new file mode 100644 index 0000000..bc76ff9 Binary files /dev/null and b/crossword_companion/sample_puzzle_images/test2b.png differ diff --git a/crossword_companion/sample_puzzle_images/test2c.png b/crossword_companion/sample_puzzle_images/test2c.png new file mode 100644 index 0000000..ebf4e3a Binary files /dev/null and b/crossword_companion/sample_puzzle_images/test2c.png differ diff --git a/crossword_companion/sample_puzzle_images/test3.png b/crossword_companion/sample_puzzle_images/test3.png new file mode 100644 index 0000000..b9663f2 Binary files /dev/null and b/crossword_companion/sample_puzzle_images/test3.png differ diff --git a/crossword_companion/specs/design.md b/crossword_companion/specs/design.md new file mode 100644 index 0000000..5139621 --- /dev/null +++ b/crossword_companion/specs/design.md @@ -0,0 +1,106 @@ +# Crossword Companion Design + +## 1. Architecture Overview + +The application follows a standard Flutter project structure, using the `firebase_ai` package for generative AI functionality. It is built to work on Android, iOS, web, and macOS. The application logic is centered around a decoupled state management system that uses three distinct `ChangeNotifier` classes to manage the UI, data, and solving process. + +- **Gemini API (AI Models):** The core AI logic is powered by two Gemini models: + - **`gemini-2.5-pro`:** A multi-modal model used for the initial, complex task of analyzing the user's crossword image(s) to infer the grid structure and all clue text. + - **`gemini-2.5-flash`:** A faster, more focused model used for the puzzle-solving step. This model is configured with a detailed system prompt to act as a crossword-solving "expert" and is called individually for each clue. + +## 2. UI/UX Design + +The application uses a single screen with a vertical `Stepper` to guide the user through the workflow. + +- **Explicit State Passing:** The `CrosswordScreen` is responsible for building the `Stepper`. It determines which step is active based on the `currentStep` from the `AppStepState`. It then passes an `isActive` boolean (`isActive: appStepState.currentStep == stepIndex`) to each step's content widget. +- **Mixin-Based Activation Logic:** To adhere to the DRY principle, the common state management logic for each stepper page is encapsulated in a `StepActivationMixin`. This mixin provides the `initState` and `didUpdateWidget` lifecycle methods, which automatically call an `onActivated` method when the step becomes active. Each step's `State` class uses this mixin, ensuring activation logic runs reliably without duplicating code. +- **Encapsulated Controls:** Each step widget is responsible for rendering its own navigation controls (e.g., "NEXT", "BACK", "SOLVE"). These controls directly call methods on the appropriate state notifiers (e.g., `appStepState.nextStep()`, `puzzleSolverState.solvePuzzle()`) to update the application's state. + +### Stepper Steps: + +1. **Select Crossword Image:** The user selects one or more images of a crossword puzzle from their device's gallery or camera. The selected images are displayed, and a "NEXT" button becomes active, allowing the user to proceed. + +2. **Verify Grid Size:** Upon entering this step, the application automatically infers the grid's dimensions from the image(s), showing a loading indicator while it works. The inferred width and height are then displayed in editable text fields for user verification or correction. The user can press "NEXT" to accept the dimensions or "BACK" to re-select the image. + +3. **Verify Grid Contents:** The app displays the inferred grid of black and white squares. The user can tap any cell to toggle its color, correcting any errors from the inference step. "NEXT" and "BACK" buttons are provided for navigation. + +4. **Verify Clue Text:** The inferred "Across" and "Down" clues are displayed in two columns for user verification. The user can tap on any clue to edit its text or number. "SOLVE" and "BACK" buttons are provided. + +5. **Solve the Puzzle:** + * Solving begins automatically when this step is entered. + * The grid is displayed on the left and fills with answers in real-time, with conflicting answers in red and matching answers in green. + * A "To Do" list on the right shows the status of each clue, including the answer and the model's confidence score. Answers that don't fit the grid are marked as "-- WRONG". + * "Pause" and "Resume" buttons allow the user to control the solving process. + * A "Restart" button appears to the right of the "Pause/Resume" button. It clears any AI-provided data while keeping all user-entered data, and starts the solving session over from the beginning. + * A "START OVER" button resets the entire workflow. + * A "BACK" button stops the solving process, clears the partial solution from the grid, and returns to the previous step. + +## 3. State Management + +The `provider` package is used for state management. The architecture follows the **Separation of Concerns** principle by dividing state into three independent `ChangeNotifier` classes, which are provided to the widget tree using `MultiProvider` in `main.dart`. + +- **`AppStepState`**: Manages the UI navigation state, specifically the `currentStep` of the `Stepper`. It exposes methods like `nextStep()`, `previousStep()`, and `reset()`. +- **`PuzzleDataState`**: Manages the lifecycle of the crossword data itself. Its responsibilities include handling image selection, triggering the AI-powered data inference, and managing user-initiated updates to the grid and clues. +- **`PuzzleSolverState`**: Dedicated entirely to the puzzle-solving process. It manages the "to-do" list of clues, orchestrates the `PuzzleSolver` service, and handles the `isSolving`, `pause`, `resume`, and `restart` states. + +This decoupled approach ensures that each part of the state is managed independently, improving maintainability and testability. + +## 4. Services + +- **`ImagePickerService`:** A wrapper around the `image_picker` package. +- **`GeminiService`:** This service handles all communication with the Gemini models. It is configured with the "expert" system prompt for the solver and has methods for: + - `inferCrosswordData(images)`: Calls `gemini-2.5-pro` to analyze one or more images. + - `solveClue(clue, length, pattern)`: Calls `gemini-2.5-flash` to get an answer and confidence score for a single clue. + - `getWordMetadata(word)`: This is a function declaration provided to the `gemini-2.5-flash` model. When the model invokes this function, the application calls the `getWordMetadataFromApi(word)` method, which queries a public dictionary API (`dictionaryapi.dev`) to retrieve grammatical information for the given word. +- **`PuzzleSolver`:** Contains the business logic for the main solving loop, iterating through clues and coordinating with the `GeminiService`, `PuzzleDataState`, and `PuzzleSolverState` to solve the puzzle. + +## 5. Puzzle Solving Logic + +The puzzle-solving process is managed by an app-driven loop within the `PuzzleSolver` service, not by an LLM agent. + +1. **Prompt Generation:** For each clue, the app calculates the required word length and the current letter pattern (e.g., `_A_`) from the grid. +2. **LLM Call:** It sends a focused prompt to the "expert" `gemini-2.5-flash` model containing only the clue text, length, and pattern. +3. **Answer Validation:** The app validates the returned answer. If the length does not match the available space, the answer is marked as wrong, and the clue is queued to be retried later. +4. **Grid Update:** Valid answers are placed on the grid. The UI uses color-coding to indicate confidence: + - **Black:** A single, uncontested answer. + - **Green:** Two matching answers (from an Across and Down clue) for the same cell. + - **Red:** Two conflicting answers for the same cell. +5. **Looping:** The app loops through all unsolved clues until the puzzle is complete, making multiple passes if necessary to retry clues that were previously answered incorrectly. + +### Handling Function Calls and Structured Output + +To ensure robust interaction with the Gemini model for clue solving, the application uses a sophisticated, multi-step process encapsulated within the `_generateJsonWithFunctionsAndSchema` helper method in `GeminiService`. This process is designed to handle both model-driven function calls and a final, strictly-formatted JSON output. + +- **Two-Model Approach:** The service uses two configurations of the `gemini-2.5-flash` model: + - `_clueSolverModelWithFunctions`: This model is configured with the `getWordMetadata` tool, allowing it to request additional information during its reasoning process. + - `_clueSolverModelWithSchema`: This model is configured with a strict JSON output schema, ensuring the final answer is always in the correct format (`{ "answer": "...", "confidence": ... }`). + +- **Chat-Based Interaction:** The process begins by starting a chat session with `_clueSolverModelWithFunctions`. + 1. The initial prompt (clue, length, pattern) is sent. + 2. The app checks the model's response for any `functionCalls`. + 3. If the model requests a function call (e.g., `getWordMetadata`), the app executes it (by calling the dictionary API) and sends the result back to the model in the same chat session. + 4. This loop continues until the model responds with its reasoning complete, without requesting further function calls. + +- **Forcing JSON Output:** Once the function-calling loop is complete, the app takes the entire chat history and uses it to make a final call to the `_clueSolverModelWithSchema`. This effectively asks the model to summarize its final conclusion from the preceding conversation into the required JSON format. + +This robust, app-driven process ensures that the model can access external tools when needed while still providing a predictable, machine-readable output for the application to consume. + +### Request Cancellation + +The `GeminiService` includes a `cancelCurrentSolve()` method, and the `solveClue` method begins with a call to it. However, in the current implementation, the underlying Gemini API calls for clue solving are made via `sendMessage` on a chat, which returns a `Future` and does not support in-flight cancellation. The `cancelCurrentSolve` method is a remnant of a previous, stream-based implementation and currently has no effect. The `PuzzleSolverState` calls this method from its `pauseSolving` and `restartSolving` methods, but it does not interrupt an ongoing `solveClue` operation. A `solveClue` call will always run to completion. + +## 6. Data Models + +- **`AppStepState`**: Manages the current step of the UI stepper. +- **`PuzzleDataState`**: Manages the crossword puzzle data, including images, grid structure, and clues. +- **`PuzzleSolverState`**: Manages the state of the puzzle-solving process. +- **`CrosswordData`:** The top-level model holding the entire puzzle's state. +- **`CrosswordGrid`:** Holds the grid's dimensions and a list of `GridCell` objects. +- **`GridCell`:** Represents a single cell. Crucially, it contains separate `acrossLetter`, `downLetter`, and `userLetter` fields to track answers from both directions and user edits, and to detect conflicts. +- **`Clue`:** Represents a single clue. +- **`ClueAnswer`:** A model to hold the string `answer` and double `confidence` returned by the LLM. +- **`TodoItem`:** Represents a clue in the UI list on the solver page, holding the clue's description, its solving status, the `ClueAnswer`, and an `isWrong` flag. + +## 7. Project Structure + +The project follows the standard structure outlined in `GEMINI.md`. \ No newline at end of file diff --git a/crossword_companion/specs/requirements.md b/crossword_companion/specs/requirements.md new file mode 100644 index 0000000..5554b6a --- /dev/null +++ b/crossword_companion/specs/requirements.md @@ -0,0 +1,50 @@ +# Crossword Companion Requirements + +## 1. Project Overview + +The application will be an open-source sample hosted on GitHub in the flutter org. It aims to demonstrate the use of Flutter, Firebase AI Logic, and Gemini to produce an agentic workflow that can solve a small crossword puzzle (one with a size under 10x10). + +## 2. Target Platforms + +The application will be built with Flutter and run on Android, iOS, web, and macOS. + +## 3. User Interface and Experience (UI/UX) + +The workflow from start to completed puzzle is presented in a single screen with individual components representing the steps of the workflow and an indicator for overall progress. At each step of the workflow, the UI should offer users the opportunity to advance or (for all steps beyond the first) roll back to a previous step. + +## 4. Agentic Workflow Steps + +The application will guide the user through the following steps: + +### 4.1. Crossword Image Input +- The app allows the user to select one or more images of an empty (unsolved) crossword puzzle from the camera or image picker. This allows for separate images of the grid and clues. +- Once chosen, the app should display the image(s), and allow the user to accept them or choose different image(s). + +*Example Grid Image:* +(The user provided an image of a grid-based crossword puzzle at ![example crossword puzzle screenshot](example-screenshot.png) + +### 4.2. Grid Size Inference & Verification +- The agent should infer the crossword dimensions from the crossword image(s). +- The agent will present the inferred height and width values to the user for verification and/or modification before continuing. + +### 4.3. Grid Contents Inference & Verification +- The agent should infer the contents of the crossword grid (cell colors, presence of numbers for answers) from the crossword image(s). +- The inferred contents will be presented to the user for verification/modification. + +### 4.4. Clue Text Inference & Verification +- The agent should infer the crossword clue text from the crossword image(s). +- The inferred clues will be presented to the user for verification. +- The user should have the option of editing a clue's number, direction, and/or text prior to advancing the workflow. + +### 4.5. Puzzle Solving +- The application should solve the puzzle by filling in answers one at a time. +- The UI should animate this process so the user can observe progress. +- The UI will display the model's confidence in each answer and visually flag answers that are invalid or conflict with other answers. +- The app will automatically backtrack and retry clues that were answered incorrectly, using the updated state of the grid to inform the new attempt. +- The UI should offer the user a mechanism to pause and resume the solving process. +- The UI should offer the user a mechanism to restart the solving process, which will clear AI-provided data, keep user-entered data, and start the solving process over from the beginning. + +### 4.6. Finished State +- Once the puzzle is solved, the application will display a "finished" message. +- The completed grid will be displayed. +- A button will be available to restart the workflow, erasing all current state. diff --git a/crossword_companion/specs/tasks.md b/crossword_companion/specs/tasks.md new file mode 100644 index 0000000..7404616 --- /dev/null +++ b/crossword_companion/specs/tasks.md @@ -0,0 +1,72 @@ +This document outlines the development tasks for building the Crossword Companion app. The tasks are structured as a series of milestones, each delivering a piece of visible functionality to the user. + +## [x] Milestone 1: Basic App Shell and UI Structure + +Create the main application window with a vertical stepper to guide the user through the workflow. All steps will be present, but initially disabled beyond the first step. + +- [x] Create the project structure as defined in `design.md`. +- [x] Implement the main screen with a `Stepper` widget containing all 5 steps from the design. +- [x] Use the `provider` package to create a basic `CrosswordState` notifier to manage the current stepper index. +- [x] Refactor the button layout on all step pages to be right-aligned, with the primary action on the far right, as specified in the design. + +## [x] Milestone 2: Select Crossword Image + +Implement the functionality for the user to select a single crossword image (containing both the grid and clues) from their device. + +- [x] Create an `ImagePickerService` to abstract the `image_picker` plugin. +- [x] Implement the UI for Step 1 ("Select Crossword Image"). +- [x] Update `CrosswordState` to hold the selected crossword image. + +## [x] Milestone 3: Grid Size Inference and Verification + +Implement the AI's ability to infer the grid dimensions from the image and allow the user to correct them. + +- [x] Set up the `firebase_ai` package and create a `GeminiService`. +- [x] Implement the `inferCrosswordData(image)` method in `GeminiService` to call the `gemini-2.5-pro` model. +- [x] Create the UI for Step 2 ("Verify Grid Size"). +- [x] Update `CrosswordState` to hold the grid dimensions. + +## [x] Milestone 4: Grid Contents Inference and Verification + +Implement the AI's ability to infer the grid's structure (cells and numbers) and allow the user to edit it. + +- [x] Create the data models: `CrosswordGrid` and `GridCell`. +- [x] Implement the UI for Step 3 ("Verify Grid Contents") that overlays the inferred grid on the original image. + - [x] Add functionality to allow users to tap on cells to set the cell to inactive (black), empty (white), or numbered with a user-provided number. +## [x] Milestone 5: Clue Text Inference and Verification + +Implement the AI's ability to infer the clue text from the crossword image and allow the user to edit it. + +- [x] Create the `Clue` data model. +- [x] Create a `ClueList` widget to display the "Across" and "Down" clues. +- [x] Implement the UI for Step 4 ("Verify Clue Text"). +- [x] Add functionality for the user to edit the clues. + +## [x] Milestone 6: Pre-Solve Validation + +Implement a validation step to ensure the integrity of the puzzle data before solving. + +- [x] Implement a validation step to ensure clue numbers match the numbers in the grid. +- [x] Display a warning to the user if there are mismatches. + +## [x] Milestone 7: Intelligent Puzzle Solving + +Implement the core puzzle-solving logic with enhanced UI, controls, and a more robust, app-driven solving strategy. + +- [x] Create the `ClueAnswer` and `TodoItem` data models. +- [x] Update `GridCell` to track `acrossLetter` and `downLetter` separately to detect conflicts. +- [x] Configure a `gemini-2.5-flash` model with a detailed system prompt to act as a crossword "expert". +- [x] Implement an app-driven solving loop in a dedicated `PuzzleSolver` service. +- [x] Implement answer validation to check if the returned word fits the grid. +- [x] Update the UI to display the answer, confidence score, and a "-- WRONG" status for invalid answers. +- [x] Update the grid UI to color-code letters based on conflicts (red), matches (green), or single entries (black). +- [x] Implement the "Pause" and "Resume" controls. +- [ ] Implement the "Restart" button. +- [x] Implement logic to auto-pause when navigating back and to reset the solution when starting a new solve. +- [x] Add JSON-based debug output to the console for monitoring the puzzle state and prompts. + +## [x] Milestone 8: Refactoring for clarity + +- [x] is there any repeated logic or structure in the stepper pages that should be refactored into a base class or a mixin? +- [x] are there other things that can be refactored to make the code more clear and readable? + \ No newline at end of file diff --git a/crossword_companion/web/favicon.png b/crossword_companion/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/crossword_companion/web/favicon.png differ diff --git a/crossword_companion/web/icons/Icon-192.png b/crossword_companion/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/crossword_companion/web/icons/Icon-192.png differ diff --git a/crossword_companion/web/icons/Icon-512.png b/crossword_companion/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/crossword_companion/web/icons/Icon-512.png differ diff --git a/crossword_companion/web/icons/Icon-maskable-192.png b/crossword_companion/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/crossword_companion/web/icons/Icon-maskable-192.png differ diff --git a/crossword_companion/web/icons/Icon-maskable-512.png b/crossword_companion/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/crossword_companion/web/icons/Icon-maskable-512.png differ diff --git a/crossword_companion/web/index.html b/crossword_companion/web/index.html new file mode 100644 index 0000000..46230c4 --- /dev/null +++ b/crossword_companion/web/index.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + crossword_companion + + + + + + + + \ No newline at end of file diff --git a/crossword_companion/web/manifest.json b/crossword_companion/web/manifest.json new file mode 100644 index 0000000..3e56ca6 --- /dev/null +++ b/crossword_companion/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "crossword_companion", + "short_name": "crossword_companion", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} \ No newline at end of file diff --git a/dash_shop/.gitignore b/dash_shop/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/dash_shop/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/dash_shop/.metadata b/dash_shop/.metadata new file mode 100644 index 0000000..ad0e577 --- /dev/null +++ b/dash_shop/.metadata @@ -0,0 +1,30 @@ +# 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: 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' diff --git a/dash_shop/README.md b/dash_shop/README.md new file mode 100644 index 0000000..1ae97bc --- /dev/null +++ b/dash_shop/README.md @@ -0,0 +1,5 @@ +# Dash Shop +A sample app that demonstrates a shopping +experience, used for an upcoming talk for +Google I/O 2026. + diff --git a/dash_shop/analysis_options.yaml b/dash_shop/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/dash_shop/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/dash_shop/assets/images/dash.png b/dash_shop/assets/images/dash.png new file mode 100644 index 0000000..39efb94 Binary files /dev/null and b/dash_shop/assets/images/dash.png differ diff --git a/dash_shop/assets/images/products/dash-backpack.png b/dash_shop/assets/images/products/dash-backpack.png new file mode 100644 index 0000000..8e0168e Binary files /dev/null and b/dash_shop/assets/images/products/dash-backpack.png differ diff --git a/dash_shop/assets/images/products/dash-hoodie.png b/dash_shop/assets/images/products/dash-hoodie.png new file mode 100644 index 0000000..cfd19f0 Binary files /dev/null and b/dash_shop/assets/images/products/dash-hoodie.png differ diff --git a/dash_shop/assets/images/products/dash-keyboard.png b/dash_shop/assets/images/products/dash-keyboard.png new file mode 100644 index 0000000..9441219 Binary files /dev/null and b/dash_shop/assets/images/products/dash-keyboard.png differ diff --git a/dash_shop/assets/images/products/dash-mug.png b/dash_shop/assets/images/products/dash-mug.png new file mode 100644 index 0000000..15a82ea Binary files /dev/null and b/dash_shop/assets/images/products/dash-mug.png differ diff --git a/dash_shop/assets/images/products/dash-phone-case.png b/dash_shop/assets/images/products/dash-phone-case.png new file mode 100644 index 0000000..90acc6b Binary files /dev/null and b/dash_shop/assets/images/products/dash-phone-case.png differ diff --git a/dash_shop/assets/images/products/dash-plush.png b/dash_shop/assets/images/products/dash-plush.png new file mode 100644 index 0000000..7c80013 Binary files /dev/null and b/dash_shop/assets/images/products/dash-plush.png differ diff --git a/dash_shop/assets/images/products/dash-stickers.png b/dash_shop/assets/images/products/dash-stickers.png new file mode 100644 index 0000000..214da77 Binary files /dev/null and b/dash_shop/assets/images/products/dash-stickers.png differ diff --git a/dash_shop/assets/payment_configurations/apple_pay_config.json b/dash_shop/assets/payment_configurations/apple_pay_config.json new file mode 100644 index 0000000..76967f1 --- /dev/null +++ b/dash_shop/assets/payment_configurations/apple_pay_config.json @@ -0,0 +1,13 @@ +{ + "provider": "apple_pay", + "data": { + "merchantIdentifier": "merchant.com.dash.shop", + "displayName": "Dash Shop", + "merchantCapabilities": ["3DS", "debit", "credit"], + "supportedNetworks": ["amex", "visa", "discover", "masterCard"], + "countryCode": "US", + "currencyCode": "USD", + "requiredBillingContactFields": ["emailAddress", "name", "phoneNumber", "postalAddress"], + "requiredShippingContactFields": ["emailAddress", "name", "phoneNumber", "postalAddress"] + } +} diff --git a/dash_shop/assets/payment_configurations/google_pay_config.json b/dash_shop/assets/payment_configurations/google_pay_config.json new file mode 100644 index 0000000..73cd2c4 --- /dev/null +++ b/dash_shop/assets/payment_configurations/google_pay_config.json @@ -0,0 +1,37 @@ +{ + "provider": "google_pay", + "data": { + "environment": "TEST", + "apiVersion": 2, + "apiVersionMinor": 0, + "allowedPaymentMethods": [ + { + "type": "CARD", + "tokenizationSpecification": { + "type": "PAYMENT_GATEWAY", + "parameters": { + "gateway": "example", + "gatewayMerchantId": "exampleGatewayMerchantId" + } + }, + "parameters": { + "allowedCardNetworks": ["VISA", "MASTERCARD"], + "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"], + "billingAddressRequired": true, + "billingAddressParameters": { + "format": "FULL", + "phoneNumberRequired": true + } + } + } + ], + "merchantInfo": { + "merchantId": "01234567890123456789", + "merchantName": "Dash Shop" + }, + "transactionInfo": { + "countryCode": "US", + "currencyCode": "USD" + } + } +} diff --git a/dash_shop/integration_test/checkout_flow_test.dart b/dash_shop/integration_test/checkout_flow_test.dart new file mode 100644 index 0000000..50f12bd --- /dev/null +++ b/dash_shop/integration_test/checkout_flow_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:dash_shop/screens/cart_screen.dart'; +import 'package:dash_shop/screens/checkout_screen.dart'; +import 'package:dash_shop/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Checkout Flow', () { + testWidgets('Add item to cart and complete checkout', (tester) async { + app.main(); + await tester.pumpAndSettle(); + + // 1. Add Dash Plushie to cart + final addPlushieButton = find.byKey(const Key('product_add_dash-plush')); + await tester.tap(addPlushieButton); + await tester.pumpAndSettle(); + + // 2. Open cart via FAB + final cartFab = find.byKey(const Key('cart_fab')); + await tester.tap(cartFab); + await tester.pumpAndSettle(); + + // 3. Verify on Cart screen and tap 'Go to checkout' + expect(find.byType(CartScreen), findsOneWidget); + final checkoutButton = find.byKey(const Key('checkoutButton')); + await tester.tap(checkoutButton); + await tester.pumpAndSettle(); + + expect(find.byType(CheckoutScreen), findsOneWidget); + + // 4. Fill in shipping information + await tester.enterText(find.byKey(const Key('fullNameField')), 'Dash'); + await tester.enterText(find.byKey(const Key('addressField')), '123 Dart Lane'); + await tester.enterText(find.byKey(const Key('cityField')), 'Mountain View'); + await tester.enterText(find.byKey(const Key('stateField')), 'CA'); + await tester.enterText(find.byKey(const Key('zipField')), '94043'); + await tester.enterText(find.byKey(const Key('phoneField')), '555-555-5555'); + + // 5. Fill in payment information + await tester.enterText(find.byKey(const Key('cardNumberField')), '1234567812345678'); + await tester.enterText(find.byKey(const Key('expiryField')), '12/25'); + await tester.enterText(find.byKey(const Key('cvvField')), '123'); + + // Scroll down to make sure Place Order is visible if needed + await tester.dragUntilVisible( + find.byKey(const Key('placeOrderButton')), + find.byType(CustomScrollView), + const Offset(0, -200), + ); + + // 6. Place order + final placeOrderButton = find.byKey(const Key('placeOrderButton')); + await tester.tap(placeOrderButton); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // 7. Verify order completion (assuming it navigates back or shows success) + // For now, we'll verify that the checkout screen is gone or a success message appeared. + // Based on my exploration, I'll see if 'Checkout' is still there. + expect(find.text('Checkout'), findsNothing); + }); + }); +} diff --git a/dash_shop/lib/main.dart b/dash_shop/lib/main.dart new file mode 100644 index 0000000..aeb8b3c --- /dev/null +++ b/dash_shop/lib/main.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:go_router/go_router.dart'; +import 'view_models/auth_view_model.dart'; +import 'view_models/cart_view_model.dart'; +import 'view_models/catalog_view_model.dart'; +import 'services/auth_api_service.dart'; +import 'services/catalog_api_service.dart'; +import 'repositories/auth_repository.dart'; +import 'repositories/catalog_repository.dart'; +import 'repositories/cart_repository.dart'; +import 'router.dart'; + +void main() { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); + }); + + final authService = AuthApiService(); + final catalogService = CatalogApiService(); + + final authRepository = AuthRepository(authService); + final catalogRepository = CatalogRepository(catalogService); + final cartRepository = CartRepository(); + + final authViewModel = AuthViewModel(authRepository); + final catalogViewModel = CatalogViewModel(catalogRepository); + final cartViewModel = CartViewModel(cartRepository); + + runApp( + DashShopApp( + authViewModel: authViewModel, + catalogViewModel: catalogViewModel, + cartViewModel: cartViewModel, + ), + ); +} + +class DashShopApp extends StatefulWidget { + final AuthViewModel authViewModel; + final CatalogViewModel catalogViewModel; + final CartViewModel cartViewModel; + + const DashShopApp({ + super.key, + required this.authViewModel, + required this.catalogViewModel, + required this.cartViewModel, + }); + + @override + State createState() => _DashShopAppState(); +} + +class _DashShopAppState extends State { + late final GoRouter _router; + + @override + void initState() { + super.initState(); + _router = createRouter( + widget.authViewModel, + widget.catalogViewModel, + widget.cartViewModel, + ); + } + + @override + Widget build(BuildContext context) { + const primaryBlack = Colors.black; + + final theme = ThemeData( + useMaterial3: true, + textTheme: GoogleFonts.interTextTheme( + Theme.of(context).textTheme, + ).apply(bodyColor: Colors.black, displayColor: Colors.black), + colorScheme: ColorScheme.fromSeed( + seedColor: primaryBlack, + primary: primaryBlack, + surface: Colors.white, + ), + scaffoldBackgroundColor: Colors.white, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + color: Colors.black, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: Colors.white, + indicatorColor: primaryBlack.withValues(alpha: 0.1), + iconTheme: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return const IconThemeData(color: primaryBlack); + } + return const IconThemeData(color: Colors.grey); + }), + labelTextStyle: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return const TextStyle( + color: primaryBlack, + fontWeight: FontWeight.bold, + ); + } + return const TextStyle(color: Colors.grey); + }), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryBlack, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + elevation: 0, + ), + ), + cardTheme: CardThemeData( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.grey.shade200), + ), + clipBehavior: Clip.antiAlias, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xFFF1F4F8), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: const BorderSide(color: Colors.black, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: const BorderSide(color: Colors.red, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: const BorderSide(color: Colors.red, width: 2), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + ), + ); + + return MaterialApp.router( + title: 'The Dash Shop', + theme: theme, + routerConfig: _router, + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/dash_shop/lib/main_test.dart b/dash_shop/lib/main_test.dart new file mode 100644 index 0000000..4b93a6e --- /dev/null +++ b/dash_shop/lib/main_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_driver/driver_extension.dart'; +import 'main.dart' as app; + +void main() { + // This line enables the extension. + enableFlutterDriverExtension(); + + // Call the main() function of the app, or an appropriate + // main() function of your UI. + app.main(); +} diff --git a/dash_shop/lib/models/cart_item.dart b/dash_shop/lib/models/cart_item.dart new file mode 100644 index 0000000..183b311 --- /dev/null +++ b/dash_shop/lib/models/cart_item.dart @@ -0,0 +1,17 @@ +import 'product.dart'; + +class CartItem { + final Product product; + final int quantity; + + const CartItem({required this.product, this.quantity = 1}); + + double get total => product.price * quantity; + + CartItem copyWith({Product? product, int? quantity}) { + return CartItem( + product: product ?? this.product, + quantity: quantity ?? this.quantity, + ); + } +} diff --git a/dash_shop/lib/models/product.dart b/dash_shop/lib/models/product.dart new file mode 100644 index 0000000..8247247 --- /dev/null +++ b/dash_shop/lib/models/product.dart @@ -0,0 +1,17 @@ +class Product { + final String id; + final String name; + final String description; + final double price; + final String imageUrl; + final String category; + + const Product({ + required this.id, + required this.name, + required this.description, + required this.price, + required this.imageUrl, + required this.category, + }); +} diff --git a/dash_shop/lib/models/user.dart b/dash_shop/lib/models/user.dart new file mode 100644 index 0000000..1dd5279 --- /dev/null +++ b/dash_shop/lib/models/user.dart @@ -0,0 +1,17 @@ +class User { + final String id; + final String name; + final String email; + final String? profileImageUrl; + final DateTime joinDate; + final int dashPoints; + + User({ + required this.id, + required this.name, + required this.email, + this.profileImageUrl, + required this.joinDate, + this.dashPoints = 0, + }); +} diff --git a/dash_shop/lib/previews.dart b/dash_shop/lib/previews.dart new file mode 100644 index 0000000..9b8893f --- /dev/null +++ b/dash_shop/lib/previews.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widget_previews.dart'; +import 'widgets/dash_button.dart'; +import 'widgets/cart_fab.dart'; +import 'widgets/product_card.dart'; +import 'models/product.dart'; +import 'repositories/cart_repository.dart'; +import 'view_models/cart_view_model.dart'; + +/// A custom preview annotation that provides the Dash Shop theme. +final class DashPreview extends Preview { + const DashPreview({ + super.name, + super.group, + super.size, + super.textScaleFactor, + super.brightness, + }) : super(theme: _themeBuilder, wrapper: _buildWrapper); + + static PreviewThemeData _themeBuilder() { + const dashBlue = Color(0xFF00B2FF); + final theme = ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: dashBlue, + primary: dashBlue, + surface: Colors.white, + ), + scaffoldBackgroundColor: const Color(0xFFF8F9FA), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: dashBlue, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + elevation: 0, + ), + ), + ); + return PreviewThemeData( + materialLight: theme, + materialDark: theme, // You could add a dark theme here if needed + ); + } + + static Widget _buildWrapper(Widget child) { + return Scaffold(body: Center(child: child)); + } +} + +// --- DashButton Previews --- + +@DashPreview(name: 'Primary Button') +Widget primaryButton() => DashButton(label: 'Add to Cart', onPressed: () {}); + +@DashPreview(name: 'Secondary Button') +Widget secondaryButton() => + DashButton(label: 'View Details', isPrimary: false, onPressed: () {}); + +@DashPreview(name: 'Disabled Button') +Widget disabledButton() => + const DashButton(label: 'Out of Stock', onPressed: null); + +// --- CartFab Previews --- + +@DashPreview(name: 'Empty Cart') +Widget emptyCart() => CartFab(viewModel: CartViewModel(CartRepository())); + +@DashPreview(name: 'Cart with Items') +Widget cartWithItems() { + final vm = CartViewModel(CartRepository()); + vm.addToCart( + Product( + id: '1', + name: 'Dash Plushie', + description: 'A cute dash plushie', + price: 19.99, + imageUrl: 'assets/images/dash.png', + category: 'Toys', + ), + ); + return CartFab(viewModel: vm); +} + +// --- ProductCard Previews --- + +@DashPreview(name: 'Product Card - Standard') +Widget productCard() => SizedBox( + width: 200, + height: 300, + child: ProductCard( + product: Product( + id: '1', + name: 'Dash Plushie', + description: 'The iconic Dash plushie, perfect for your desk.', + price: 19.99, + imageUrl: 'assets/images/products/dash-plush.png', + category: 'Toys', + ), + quantity: 0, + onAdd: () {}, + onRemove: () {}, + onTap: () {}, + ), +); + +@DashPreview(name: 'Product Card - Keyboard') +Widget productCardKeyboard() => SizedBox( + width: 200, + height: 300, + child: ProductCard( + product: Product( + id: '2', + name: 'Mechanical Keyboard', + description: 'A high-quality mechanical keyboard for coding.', + price: 129.99, + imageUrl: 'assets/images/products/dash-keyboard.png', + category: 'Gadgets', + ), + quantity: 0, + onAdd: () {}, + onRemove: () {}, + onTap: () {}, + ), +); diff --git a/dash_shop/lib/repositories/auth_repository.dart b/dash_shop/lib/repositories/auth_repository.dart new file mode 100644 index 0000000..70f4eb6 --- /dev/null +++ b/dash_shop/lib/repositories/auth_repository.dart @@ -0,0 +1,43 @@ +import '../models/user.dart'; +import '../services/auth_api_service.dart'; + +class AuthRepository { + final AuthApiService _apiService; + User? _currentUser; + + AuthRepository(this._apiService); + + User? get currentUser => _currentUser; + bool get isAuthenticated => _currentUser != null; + + Future checkAuthStatus() async { + try { + final rawData = await _apiService.fetchCurrentUserRaw(); + _currentUser = _mapUser(rawData); + return _currentUser; + } catch (_) { + _currentUser = null; + return null; + } + } + + Future signIn(String name, String email) async { + final rawData = await _apiService.fetchUserRaw(name, email); + _currentUser = _mapUser(rawData); + return _currentUser; + } + + void signOut() { + _currentUser = null; + } + + User _mapUser(Map json) { + return User( + id: json['id'], + name: json['name'], + email: json['email'], + joinDate: DateTime.parse(json['joinDate']), + dashPoints: json['dashPoints'], + ); + } +} diff --git a/dash_shop/lib/repositories/cart_repository.dart b/dash_shop/lib/repositories/cart_repository.dart new file mode 100644 index 0000000..4315aa6 --- /dev/null +++ b/dash_shop/lib/repositories/cart_repository.dart @@ -0,0 +1,42 @@ +import 'package:flutter/foundation.dart'; +import '../models/product.dart'; +import '../models/cart_item.dart'; + +class CartRepository extends ChangeNotifier { + final Map _items = {}; + + Map get items => Map.unmodifiable(_items); + + void addToCart(Product product) { + if (_items.containsKey(product.id)) { + _items[product.id] = _items[product.id]!.copyWith( + quantity: _items[product.id]!.quantity + 1, + ); + } else { + _items[product.id] = CartItem(product: product); + } + notifyListeners(); + } + + void removeFromCart(String productId) { + if (_items.containsKey(productId)) { + if (_items[productId]!.quantity > 1) { + _items[productId] = _items[productId]!.copyWith( + quantity: _items[productId]!.quantity - 1, + ); + } else { + _items.remove(productId); + } + notifyListeners(); + } + } + + void clear() { + _items.clear(); + notifyListeners(); + } + + double get subtotal => _items.values.fold(0, (sum, item) => sum + item.total); + int get itemCount => + _items.values.fold(0, (sum, item) => sum + item.quantity); +} diff --git a/dash_shop/lib/repositories/catalog_repository.dart b/dash_shop/lib/repositories/catalog_repository.dart new file mode 100644 index 0000000..3170479 --- /dev/null +++ b/dash_shop/lib/repositories/catalog_repository.dart @@ -0,0 +1,32 @@ +import '../models/product.dart'; +import '../services/catalog_api_service.dart'; + +class CatalogRepository { + final CatalogApiService _apiService; + List? _cachedProducts; + + CatalogRepository(this._apiService); + + Future> getProducts() async { + if (_cachedProducts != null) { + return _cachedProducts!; + } + + final rawData = await _apiService.fetchProductsRaw(); + final products = rawData + .map( + (json) => Product( + id: json['id'], + name: json['name'], + description: json['description'], + price: json['price'], + imageUrl: json['imageUrl'], + category: json['category'], + ), + ) + .toList(); + + _cachedProducts = products; + return products; + } +} diff --git a/dash_shop/lib/router.dart b/dash_shop/lib/router.dart new file mode 100644 index 0000000..501fc13 --- /dev/null +++ b/dash_shop/lib/router.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'screens/cart_screen.dart'; +import 'screens/catalog_screen.dart'; +import 'screens/checkout_screen.dart'; +import 'screens/home_screen.dart'; +import 'screens/product_detail_screen.dart'; +import 'screens/profile_screen.dart'; +import 'view_models/auth_view_model.dart'; +import 'view_models/cart_view_model.dart'; +import 'view_models/catalog_view_model.dart'; + +final rootNavigatorKey = GlobalKey(); +final shellNavigatorHomeKey = GlobalKey(); +final shellNavigatorShopKey = GlobalKey(); +final shellNavigatorCartKey = GlobalKey(); +final shellNavigatorProfileKey = GlobalKey(); + +GoRouter createRouter( + AuthViewModel authViewModel, + CatalogViewModel catalogViewModel, + CartViewModel cartViewModel, +) { + return GoRouter( + navigatorKey: rootNavigatorKey, + initialLocation: '/', + routes: [ + StatefulShellRoute.indexedStack( + builder: (context, state, navigationShell) { + return Scaffold( + body: navigationShell, + bottomNavigationBar: NavigationBar( + selectedIndex: navigationShell.currentIndex, + destinations: const [ + NavigationDestination(icon: Icon(Icons.home), label: 'Home'), + NavigationDestination(icon: Icon(Icons.shop), label: 'Shop'), + NavigationDestination( + icon: Icon(Icons.shopping_cart), + label: 'Cart', + ), + NavigationDestination( + icon: Icon(Icons.person), + label: 'Profile', + ), + ], + onDestinationSelected: (index) => navigationShell.goBranch(index), + ), + ); + }, + branches: [ + StatefulShellBranch( + navigatorKey: shellNavigatorHomeKey, + routes: [ + GoRoute( + path: '/', + builder: (context, state) => HomeScreen( + catalogViewModel: catalogViewModel, + cartViewModel: cartViewModel, + ), + ), + ], + ), + StatefulShellBranch( + navigatorKey: shellNavigatorShopKey, + routes: [ + GoRoute( + path: '/shop', + builder: (context, state) => CatalogScreen( + viewModel: catalogViewModel, + cartViewModel: cartViewModel, + initialCategory: state.uri.queryParameters['category'], + ), + ), + ], + ), + StatefulShellBranch( + navigatorKey: shellNavigatorCartKey, + routes: [ + GoRoute( + path: '/cart', + builder: (context, state) => + CartScreen(viewModel: cartViewModel), + ), + ], + ), + StatefulShellBranch( + navigatorKey: shellNavigatorProfileKey, + routes: [ + GoRoute( + path: '/profile', + builder: (context, state) => + ProfileScreen(viewModel: authViewModel), + ), + ], + ), + ], + ), + GoRoute( + path: '/checkout', + parentNavigatorKey: rootNavigatorKey, + builder: (context, state) => + CheckoutScreen(cartViewModel: cartViewModel), + ), + GoRoute( + path: '/shop/product/:id', + parentNavigatorKey: rootNavigatorKey, + builder: (context, state) => ProductDetailScreen( + productId: state.pathParameters['id']!, + viewModel: catalogViewModel, + cartViewModel: cartViewModel, + ), + ), + ], + ); +} diff --git a/dash_shop/lib/screens/cart_screen.dart b/dash_shop/lib/screens/cart_screen.dart new file mode 100644 index 0000000..0dba2b4 --- /dev/null +++ b/dash_shop/lib/screens/cart_screen.dart @@ -0,0 +1,448 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../view_models/cart_view_model.dart'; + +class CartScreen extends StatelessWidget { + final CartViewModel viewModel; + + const CartScreen({super.key, required this.viewModel}); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + final items = viewModel.items.values.toList(); + final subtotal = viewModel.subtotal; + final deliveryFee = items.isEmpty ? 0.0 : 3.00; + final taxes = items.isEmpty ? 0.0 : 2.50; + final total = subtotal + deliveryFee + taxes; + + return Scaffold( + backgroundColor: Colors.white, + body: LayoutBuilder( + builder: (context, constraints) { + final isLargeScreen = constraints.maxWidth > 800; + + final cartListWidget = CustomScrollView( + slivers: [ + SliverAppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + pinned: true, + automaticallyImplyLeading: false, + expandedHeight: 120, + flexibleSpace: FlexibleSpaceBar( + centerTitle: false, + titlePadding: const EdgeInsetsDirectional.only( + start: 24, + bottom: 12, + ), + title: Text( + 'Cart', + style: GoogleFonts.playfairDisplay( + fontSize: 32, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + color: Colors.black, + ), + ), + ), + actions: [ + IconButton( + icon: const Icon( + Icons.delete_outline, + color: Colors.black, + ), + onPressed: () => viewModel.clear(), + ), + const SizedBox(width: 8), + ], + ), + if (items.isEmpty) + const SliverFillRemaining( + child: Center( + child: Text( + 'Your cart is empty', + style: TextStyle(color: Colors.grey), + ), + ), + ) + else + SliverPadding( + padding: EdgeInsets.only( + left: 24, + right: isLargeScreen ? 360 + 24 + 24 : 24, + top: 16, + bottom: 16, + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate(( + context, + index, + ) { + if (index.isOdd) { + return const SizedBox(height: 24); + } + final itemIndex = index ~/ 2; + final item = items[itemIndex]; + return Row( + children: [ + Container( + width: 88, + height: 88, + decoration: BoxDecoration( + color: const Color(0xFFF1F4F8), + borderRadius: BorderRadius.circular(24), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Image.asset( + item.product.imageUrl, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => Icon( + item.product.name.contains('Hoodie') + ? Icons.checkroom + : Icons.shopping_bag, + color: Colors.grey.shade400, + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: isLargeScreen + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + item.product.name, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '\$${item.product.price.toStringAsFixed(2)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.black, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + item.product.category, + style: const TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () => viewModel.removeFromCart( + item.product.id, + ), + child: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Icon( + Icons.remove, + size: 16, + color: Colors.black, + ), + ), + ), + Text( + '${item.quantity}', + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + InkWell( + onTap: () => viewModel.addToCart( + item.product, + ), + child: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Icon( + Icons.add, + size: 16, + color: Colors.black, + ), + ), + ), + ], + ), + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.product.name, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + item.product.category, + style: const TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '\$${item.product.price.toStringAsFixed(2)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.black, + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, + ), + child: Row( + children: [ + InkWell( + onTap: () => viewModel.removeFromCart( + item.product.id, + ), + child: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Icon( + Icons.remove, + size: 16, + color: Colors.black, + ), + ), + ), + Text( + '${item.quantity}', + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + InkWell( + onTap: () => viewModel.addToCart( + item.product, + ), + child: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Icon( + Icons.add, + size: 16, + color: Colors.black, + ), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ); + }, childCount: items.length * 2 - 1), + ), + ), + ], + ); + + Widget? summaryWidget; + if (items.isNotEmpty) { + summaryWidget = Container( + width: isLargeScreen ? 360 : double.infinity, + margin: isLargeScreen + ? const EdgeInsets.only(top: 136, right: 24, bottom: 24) + : null, + padding: isLargeScreen + ? const EdgeInsets.all(24) + : const EdgeInsets.fromLTRB(24, 0, 24, 32), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: isLargeScreen ? BorderRadius.circular(16) : null, + border: isLargeScreen + ? Border.all(color: Colors.grey.shade200) + : null, + boxShadow: isLargeScreen + ? [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ] + : null, + ), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isLargeScreen) + const Divider(height: 32, color: Color(0xFFEEEEEE)), + _SummaryRow(label: 'Subtotal', value: subtotal), + const SizedBox(height: 12), + _SummaryRow(label: 'Delivery Fee', value: deliveryFee), + const SizedBox(height: 12), + _SummaryRow(label: 'Taxes', value: taxes), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + Text( + '\$${total.toStringAsFixed(2)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + color: Colors.black, + ), + ), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + key: const Key('checkoutButton'), + onPressed: () => context.push('/checkout'), + child: const Text( + 'Go to checkout', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + } + + if (isLargeScreen) { + return Stack( + children: [ + cartListWidget, + if (summaryWidget != null) + Align( + alignment: Alignment.topRight, + child: summaryWidget, + ), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: cartListWidget), + ?summaryWidget, + ], + ); + }, + ), + ); + }, + ); + } +} + +class _SummaryRow extends StatelessWidget { + final String label; + final double value; + + const _SummaryRow({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(color: Colors.grey, fontSize: 16)), + Text( + '\$${value.toStringAsFixed(2)}', + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Colors.black, + ), + ), + ], + ); + } +} diff --git a/dash_shop/lib/screens/catalog_screen.dart b/dash_shop/lib/screens/catalog_screen.dart new file mode 100644 index 0000000..74c0ae0 --- /dev/null +++ b/dash_shop/lib/screens/catalog_screen.dart @@ -0,0 +1,293 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../view_models/cart_view_model.dart'; +import '../view_models/catalog_view_model.dart'; +import '../widgets/product_card.dart'; +import '../widgets/cart_fab.dart'; + +class CatalogScreen extends StatefulWidget { + final CatalogViewModel viewModel; + final CartViewModel cartViewModel; + final String? initialCategory; + + const CatalogScreen({ + super.key, + required this.viewModel, + required this.cartViewModel, + this.initialCategory, + }); + + @override + State createState() => _CatalogScreenState(); +} + +class _CatalogScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + late String _selectedCategory; + + @override + void initState() { + super.initState(); + _selectedCategory = widget.initialCategory ?? 'All'; + } + + @override + void didUpdateWidget(CatalogScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialCategory != oldWidget.initialCategory && + widget.initialCategory != null) { + setState(() { + _selectedCategory = widget.initialCategory!; + }); + } + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: widget.viewModel, + builder: (context, _) { + if (widget.viewModel.isLoading) { + return const Scaffold( + backgroundColor: Colors.white, + body: Center(child: CircularProgressIndicator()), + ); + } + final allProducts = widget.viewModel.products; + + // Filtering Logic + final filteredProducts = allProducts.where((product) { + final matchesCategory = + _selectedCategory == 'All' || + product.category == _selectedCategory; + final matchesSearch = product.name.toLowerCase().contains( + _searchController.text.toLowerCase(), + ); + return matchesCategory && matchesSearch; + }).toList(); + + return Scaffold( + backgroundColor: Colors.white, + floatingActionButton: CartFab(viewModel: widget.cartViewModel), + body: CustomScrollView( + slivers: [ + SliverAppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + pinned: true, + expandedHeight: 100, + flexibleSpace: FlexibleSpaceBar( + centerTitle: false, + titlePadding: const EdgeInsetsDirectional.only( + start: 16, + bottom: 12, + ), + title: Text( + 'Shop', + style: GoogleFonts.playfairDisplay( + fontSize: 32, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + letterSpacing: -0.5, + color: Colors.black, + ), + ), + ), + ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 16), + child: Row( + children: [ + const Icon( + Icons.location_on, + size: 16, + color: Colors.black, + ), + const SizedBox(width: 4), + Text( + '1600 Amphitheatre Parkway', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade800, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + + // Search Bar + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: TextField( + controller: _searchController, + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + hintText: 'What are you looking for?', + hintStyle: TextStyle(color: Colors.grey.shade600), + prefixIcon: + const Icon(Icons.search, color: Colors.black), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon( + Icons.clear, + size: 20, + color: Colors.black, + ), + onPressed: () { + _searchController.clear(); + setState(() {}); + }, + ) + : null, + contentPadding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 16, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: const Color(0xFFF3F3F3), + ), + ), + ), + + // Category Selectors + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: + [ + 'All', + 'Keyboards', + 'Plushies', + 'Stickers', + 'Apparel', + 'Home', + ] + .map( + (category) => _FilterChip( + label: category, + isSelected: _selectedCategory == category, + onTap: () => setState( + () => _selectedCategory = category, + ), + ), + ) + .toList(), + ), + ), + ), + ], + ), + ), + + // Product Grid + if (filteredProducts.isEmpty) + const SliverFillRemaining( + child: Center( + child: Text('No products found matching your search.'), + ), + ) + else + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverGrid( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final product = filteredProducts[index]; + final cartItem = + widget.cartViewModel.items[product.id]; + final quantity = cartItem?.quantity ?? 0; + + return ProductCard( + product: product, + quantity: quantity, + onAdd: () => + widget.cartViewModel.addToCart(product), + onRemove: () => + widget.cartViewModel.removeFromCart(product.id), + onTap: () => + context.push('/shop/product/${product.id}'), + ); + }, + childCount: filteredProducts.length, + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _FilterChip extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback onTap; + + const _FilterChip({ + required this.label, + required this.onTap, + this.isSelected = false, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? Colors.black : const Color(0xFFF3F3F3), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : Colors.black, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + fontSize: 14, + ), + ), + ), + ), + ); + } +} diff --git a/dash_shop/lib/screens/checkout_screen.dart b/dash_shop/lib/screens/checkout_screen.dart new file mode 100644 index 0000000..90f0649 --- /dev/null +++ b/dash_shop/lib/screens/checkout_screen.dart @@ -0,0 +1,809 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:pay/pay.dart'; +import '../view_models/cart_view_model.dart'; +import '../utils/formatters.dart'; + +class CheckoutScreen extends StatefulWidget { + final CartViewModel cartViewModel; + + const CheckoutScreen({super.key, required this.cartViewModel}); + + @override + State createState() => _CheckoutScreenState(); +} + +class _CheckoutScreenState extends State { + final _formKey = GlobalKey(); + final _scrollController = ScrollController(); + int _currentStep = 0; // 0: Shipping, 1: Payment, 2: Review + + // Controllers for section completion tracking + final _nameController = TextEditingController(); + final _addressController = TextEditingController(); + final _cityController = TextEditingController(); + final _stateController = TextEditingController(); + final _zipController = TextEditingController(); + final _phoneController = TextEditingController(); + + final _cardNumberController = TextEditingController(); + final _expiryController = TextEditingController(); + final _cvvController = TextEditingController(); + + // Focus nodes + final _nameFocus = FocusNode(); + final _addressFocus = FocusNode(); + final _cityFocus = FocusNode(); + final _stateFocus = FocusNode(); + final _zipFocus = FocusNode(); + final _phoneFocus = FocusNode(); + + final _cardNumberFocus = FocusNode(); + final _expiryFocus = FocusNode(); + final _cvvFocus = FocusNode(); + + late final Future _payClientFuture; + + @override + void initState() { + super.initState(); + // Add listeners to controllers to update step progress + final shippingControllers = [ + _nameController, + _addressController, + _cityController, + _stateController, + _zipController, + _phoneController, + ]; + for (var controller in shippingControllers) { + controller.addListener(_checkShippingCompletion); + } + + final paymentControllers = [ + _cardNumberController, + _expiryController, + _cvvController, + ]; + for (var controller in paymentControllers) { + controller.addListener(_checkPaymentCompletion); + } + + _payClientFuture = _initPay(); + } + + Future _initPay() async { + final googlePayConfig = await PaymentConfiguration.fromAsset( + 'payment_configurations/google_pay_config.json', + ); + final applePayConfig = await PaymentConfiguration.fromAsset( + 'payment_configurations/apple_pay_config.json', + ); + return Pay({ + PayProvider.google_pay: googlePayConfig, + PayProvider.apple_pay: applePayConfig, + }); + } + + void _checkShippingCompletion() { + final isShippingValid = + _nameController.text.isNotEmpty && + _addressController.text.isNotEmpty && + _cityController.text.isNotEmpty && + _stateController.text.isNotEmpty && + _zipController.text.isNotEmpty && + _phoneController.text.isNotEmpty; + + if (isShippingValid && _currentStep == 0) { + setState(() => _currentStep = 1); + } else if (!isShippingValid && _currentStep == 1) { + setState(() => _currentStep = 0); + } + } + + void _checkPaymentCompletion() { + final isPaymentValid = + _cardNumberController.text.isNotEmpty && + _expiryController.text.isNotEmpty && + _cvvController.text.isNotEmpty; + + if (isPaymentValid && _currentStep <= 1) { + setState(() => _currentStep = 2); + } else if (!isPaymentValid && _currentStep == 2) { + _checkShippingCompletion(); // Fallback to check if still at payment or shipping + if (_currentStep != 0) { + setState(() => _currentStep = 1); + } + } + } + + List get _paymentItems { + final subtotal = widget.cartViewModel.subtotal; + const shipping = 5.0; + final total = subtotal + shipping; + + return [ + PaymentItem( + label: 'Subtotal', + amount: subtotal.toStringAsFixed(2), + status: PaymentItemStatus.final_price, + ), + const PaymentItem( + label: 'Shipping', + amount: '5.00', + status: PaymentItemStatus.final_price, + ), + PaymentItem( + label: 'Total', + amount: total.toStringAsFixed(2), + status: PaymentItemStatus.final_price, + ), + ]; + } + + void _onApplePayResult(Map result) { + debugPrint('Apple Pay result: $result'); + _handlePaymentSuccess(); + } + + void _onGooglePayResult(Map result) { + debugPrint('Google Pay result: $result'); + _handlePaymentSuccess(); + } + + void _onPaymentPressed(Pay provider, PayProvider providerType) async { + try { + final result = await provider.showPaymentSelector( + providerType, + _paymentItems, + ); + if (providerType == PayProvider.google_pay) { + _onGooglePayResult(result); + } else { + _onApplePayResult(result); + } + } catch (e) { + debugPrint('Error during payment: $e'); + } + } + + void _handlePaymentSuccess() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Order placed successfully!'), + backgroundColor: Colors.green, + ), + ); + widget.cartViewModel.clear(); + context.go('/'); + } + + @override + void dispose() { + _scrollController.dispose(); + _nameController.dispose(); + _addressController.dispose(); + _cityController.dispose(); + _stateController.dispose(); + _zipController.dispose(); + _phoneController.dispose(); + _cardNumberController.dispose(); + _expiryController.dispose(); + _cvvController.dispose(); + + _nameFocus.dispose(); + _addressFocus.dispose(); + _cityFocus.dispose(); + _stateFocus.dispose(); + _zipFocus.dispose(); + _phoneFocus.dispose(); + _cardNumberFocus.dispose(); + _expiryFocus.dispose(); + _cvvFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: const Key('checkoutScaffold'), + backgroundColor: Colors.white, + body: Form( + key: _formKey, + child: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppBar( + pinned: true, + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.close, color: Colors.black), + onPressed: () => context.pop(), + ), + expandedHeight: 120, + flexibleSpace: FlexibleSpaceBar( + centerTitle: false, + titlePadding: const EdgeInsetsDirectional.only( + start: 56, + bottom: 12, + ), + title: Text( + 'Checkout', + style: GoogleFonts.playfairDisplay( + fontSize: 32, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + color: Colors.black, + ), + ), + ), + ), + + // Interactive Stepper + SliverPersistentHeader( + pinned: true, + delegate: _StepperHeaderDelegate(currentStep: _currentStep), + ), + + SliverPadding( + padding: const EdgeInsets.all(24.0), + sliver: SliverList( + delegate: SliverChildListDelegate([ + _SectionHeader( + title: 'Shipping Address', + icon: Icons.local_shipping_outlined, + ), + const SizedBox(height: 16), + _buildShippingForm(), + + const SizedBox(height: 32), + _SectionHeader( + title: 'Payment Information', + icon: Icons.payment_outlined, + ), + const SizedBox(height: 16), + _buildPaymentForm(), + + const SizedBox(height: 32), + _SectionHeader( + title: 'Review Order', + icon: Icons.receipt_long_outlined, + ), + const SizedBox(height: 16), + _buildOrderReview(), + + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + child: ElevatedButton( + key: const Key('placeOrderButton'), + onPressed: _handlePlaceOrder, + child: const Text( + 'Place Order', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(height: 48), + ]), + ), + ), + ], + ), + ), + ); + } + + Widget _buildShippingForm() { + return Column( + children: [ + TextFormField( + key: const Key('fullNameField'), + controller: _nameController, + focusNode: _nameFocus, + decoration: const InputDecoration( + labelText: 'Full Name', + prefixIcon: Icon(Icons.person_outline), + ), + validator: (v) => v?.isEmpty ?? true ? 'Required' : null, + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => + FocusScope.of(context).requestFocus(_addressFocus), + ), + const SizedBox(height: 12), + TextFormField( + key: const Key('addressField'), + controller: _addressController, + focusNode: _addressFocus, + decoration: const InputDecoration( + labelText: 'Address Line 1', + prefixIcon: Icon(Icons.home_outlined), + ), + validator: (v) => v?.isEmpty ?? true ? 'Required' : null, + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => + FocusScope.of(context).requestFocus(_cityFocus), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextFormField( + key: const Key('cityField'), + controller: _cityController, + focusNode: _cityFocus, + decoration: const InputDecoration(labelText: 'City'), + validator: (v) => v?.isEmpty ?? true ? 'Required' : null, + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => + FocusScope.of(context).requestFocus(_stateFocus), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + key: const Key('stateField'), + controller: _stateController, + focusNode: _stateFocus, + decoration: const InputDecoration(labelText: 'State'), + validator: (v) => v?.isEmpty ?? true ? 'Required' : null, + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => + FocusScope.of(context).requestFocus(_zipFocus), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextFormField( + key: const Key('zipField'), + controller: _zipController, + focusNode: _zipFocus, + decoration: const InputDecoration(labelText: 'ZIP Code'), + validator: (v) => v?.isEmpty ?? true ? 'Required' : null, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(5), + ], + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => + FocusScope.of(context).requestFocus(_phoneFocus), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + key: const Key('phoneField'), + controller: _phoneController, + focusNode: _phoneFocus, + decoration: const InputDecoration(labelText: 'Phone'), + validator: (v) => v?.isEmpty ?? true ? 'Required' : null, + keyboardType: TextInputType.phone, + inputFormatters: [PhoneNumberFormatter()], + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => + FocusScope.of(context).requestFocus(_cardNumberFocus), + ), + ), + ], + ), + ], + ); + } + + Widget _buildPaymentForm() { + return FutureBuilder( + future: _payClientFuture, + builder: (context, paySnapshot) { + if (!paySnapshot.hasData) return const SizedBox.shrink(); + final payClient = paySnapshot.data!; + + return Column( + children: [ + FutureBuilder( + future: payClient.userCanPay(PayProvider.google_pay), + builder: (context, snapshot) => snapshot.data == true + ? Padding( + padding: const EdgeInsets.only(top: 15.0), + child: SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () => + _onPaymentPressed(payClient, PayProvider.google_pay), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: Colors.grey.shade300), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Pay with Google', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ) + : const SizedBox.shrink(), + ), + FutureBuilder( + future: payClient.userCanPay(PayProvider.apple_pay), + builder: (context, snapshot) => snapshot.data == true + ? Padding( + padding: const EdgeInsets.only(top: 15.0), + child: SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () => + _onPaymentPressed(payClient, PayProvider.apple_pay), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: Colors.grey.shade300), + backgroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Pay with Apple', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ) + : const SizedBox.shrink(), + ), + const SizedBox(height: 32), + const Row( + children: [ + Expanded(child: Divider()), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'OR', + style: TextStyle( + color: Colors.grey, + fontWeight: FontWeight.w500, + fontSize: 12, + ), + ), + ), + Expanded(child: Divider()), + ], + ), + const SizedBox(height: 32), + TextFormField( + key: const Key('cardNumberField'), + controller: _cardNumberController, + focusNode: _cardNumberFocus, + decoration: const InputDecoration( + labelText: 'Card Number', + prefixIcon: Icon(Icons.credit_card), + hintText: '**** **** **** ****', + ), + validator: (v) => v?.isEmpty ?? true ? 'Required' : null, + keyboardType: TextInputType.number, + inputFormatters: [CardNumberFormatter()], + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => + FocusScope.of(context).requestFocus(_expiryFocus), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextFormField( + key: const Key('expiryField'), + controller: _expiryController, + focusNode: _expiryFocus, + decoration: const InputDecoration( + labelText: 'Expiry Date', + hintText: 'MM/YY', + ), + validator: (v) => v?.isEmpty ?? true ? 'Required' : null, + keyboardType: TextInputType.datetime, + inputFormatters: [ExpirationDateFormatter()], + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => + FocusScope.of(context).requestFocus(_cvvFocus), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + key: const Key('cvvField'), + controller: _cvvController, + focusNode: _cvvFocus, + decoration: const InputDecoration( + labelText: 'CVV', + hintText: '***', + ), + validator: (v) => v?.isEmpty ?? true ? 'Required' : null, + keyboardType: TextInputType.number, + inputFormatters: [CVVFormatter(maxLength: 3)], + obscureText: true, + textInputAction: TextInputAction.done, + ), + ), + ], + ), + ], + ); + }, + ); + } + + Widget _buildOrderReview() { + final subtotal = widget.cartViewModel.subtotal; + const shipping = 5.00; + final total = subtotal + shipping; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.grey.shade200), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + ...widget.cartViewModel.items.values.map( + (item) => Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${item.quantity}x ${item.product.name}'), + Text( + '\$${(item.product.price * item.quantity).toStringAsFixed(2)}', + ), + ], + ), + ), + ), + const Divider(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Subtotal'), + Text('\$${subtotal.toStringAsFixed(2)}'), + ], + ), + const SizedBox(height: 8), + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('Shipping'), Text('\$5.00')], + ), + const Divider(height: 24, thickness: 1), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + Text( + '\$${total.toStringAsFixed(2)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.blue, + ), + ), + ], + ), + ], + ), + ), + ); + } + + void _handlePlaceOrder() { + if (_formKey.currentState?.validate() ?? false) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Order placed successfully!'), + backgroundColor: Colors.green, + ), + ); + widget.cartViewModel.clear(); + context.go('/'); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please correct the errors in the form.'), + backgroundColor: Colors.redAccent, + ), + ); + } + } +} + +class _SectionHeader extends StatelessWidget { + final String title; + final IconData icon; + + const _SectionHeader({required this.title, required this.icon}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Icon(icon, size: 20, color: Colors.blue), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ); + } +} + +class _StepperHeaderDelegate extends SliverPersistentHeaderDelegate { + final int currentStep; + + _StepperHeaderDelegate({required this.currentStep}); + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return Container( + color: Colors.white, + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32), + decoration: BoxDecoration( + color: const Color(0xFFE6F7FF), + border: Border( + bottom: BorderSide(color: Colors.blue.withValues(alpha: 0.1)), + ), + ), + child: Row( + children: [ + _StepIndicator( + label: 'Shipping', + isCompleted: currentStep > 0, + isActive: currentStep == 0, + ), + Expanded( + child: _AnimatedDivider( + isActive: currentStep > 0, + color: Theme.of(context).primaryColor, + ), + ), + _StepIndicator( + label: 'Payment', + isCompleted: currentStep > 1, + isActive: currentStep == 1, + ), + Expanded( + child: _AnimatedDivider( + isActive: currentStep > 1, + color: Theme.of(context).primaryColor, + ), + ), + _StepIndicator(label: 'Review', isActive: currentStep == 2), + ], + ), + ), + ], + ), + ); + } + + @override + double get maxExtent => 80; + + @override + double get minExtent => 80; + + @override + bool shouldRebuild(covariant _StepperHeaderDelegate oldDelegate) => + oldDelegate.currentStep != currentStep; +} + +class _AnimatedDivider extends StatelessWidget { + final bool isActive; + final Color color; + + const _AnimatedDivider({required this.isActive, required this.color}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container(height: 2, color: Colors.grey.shade300), + AnimatedContainer( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + height: 2, + width: isActive + ? 500 + : 0, // Should be roughly enough to fill the Expanded space + color: color, + ), + ], + ); + } +} + +class _StepIndicator extends StatelessWidget { + final String label; + final bool isCompleted; + final bool isActive; + + const _StepIndicator({ + required this.label, + this.isCompleted = false, + this.isActive = false, + }); + + @override + Widget build(BuildContext context) { + final color = isCompleted || isActive + ? Theme.of(context).primaryColor + : Colors.grey; + return Column( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: 24, + height: 24, + decoration: BoxDecoration( + color: isCompleted ? color : Colors.white, + border: Border.all(color: color, width: 2), + shape: BoxShape.circle, + boxShadow: isActive + ? [ + BoxShadow( + color: color.withValues(alpha: 0.3), + blurRadius: 8, + spreadRadius: 2, + ), + ] + : null, + ), + child: isCompleted + ? const Icon(Icons.check, color: Colors.white, size: 14) + : null, + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 10, + color: color, + fontWeight: isActive || isCompleted + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ); + } +} diff --git a/dash_shop/lib/screens/home_screen.dart b/dash_shop/lib/screens/home_screen.dart new file mode 100644 index 0000000..8ed89c2 --- /dev/null +++ b/dash_shop/lib/screens/home_screen.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../view_models/cart_view_model.dart'; +import '../view_models/catalog_view_model.dart'; +import '../widgets/product_card.dart'; + +import 'package:google_fonts/google_fonts.dart'; + +import '../widgets/cart_fab.dart'; + +class HomeScreen extends StatelessWidget { + final CatalogViewModel catalogViewModel; + final CartViewModel cartViewModel; + + const HomeScreen({ + super.key, + required this.catalogViewModel, + required this.cartViewModel, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + floatingActionButton: CartFab(viewModel: cartViewModel), + body: ListenableBuilder( + listenable: Listenable.merge([catalogViewModel, cartViewModel]), + builder: (context, _) { + return CustomScrollView( + slivers: [ + SliverAppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + pinned: true, + expandedHeight: 120, + flexibleSpace: FlexibleSpaceBar( + centerTitle: false, + titlePadding: const EdgeInsetsDirectional.only( + start: 16, + bottom: 12, + ), + title: Text( + 'Dash Shop', + style: GoogleFonts.playfairDisplay( + fontSize: 32, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + letterSpacing: -0.5, + color: Colors.black, + ), + ), + ), + ), + // Featured Banner + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + height: 180, + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFFF1F4F8), + borderRadius: BorderRadius.circular(16), + ), + child: Stack( + children: [ + Positioned( + right: 0, + bottom: 0, + top: 0, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Image.asset( + 'assets/images/products/dash-keyboard.png', + width: 140, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => + Icon( + Icons.phone_iphone, + size: 120, + color: Colors.grey.shade400, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'New Merch!', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + height: 1.2, + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => context.go('/shop'), + style: ElevatedButton.styleFrom( + minimumSize: const Size(120, 36), + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + ), + child: const Text('Shop Now'), + ), + ], + ), + ), + ], + ), + ), + ), + ), + + // Trending Now Title + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 16), + child: Text( + 'Trending Now', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ), + + if (catalogViewModel.products.isEmpty) + const SliverFillRemaining( + child: Center( + child: CircularProgressIndicator(), + ), + ) + else + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverGrid( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final product = catalogViewModel + .products[index % catalogViewModel.products.length]; + final cartItem = cartViewModel.items[product.id]; + final quantity = cartItem?.quantity ?? 0; + return ProductCard( + product: product, + quantity: quantity, + onAdd: () { + cartViewModel.addToCart(product); + }, + onRemove: () { + cartViewModel.removeFromCart(product.id); + }, + onTap: () => + context.push('/shop/product/${product.id}'), + ); + }, + childCount: 4, + ), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 24)), + ], + ); + }, + ), + ); + } +} diff --git a/dash_shop/lib/screens/product_detail_screen.dart b/dash_shop/lib/screens/product_detail_screen.dart new file mode 100644 index 0000000..b76cc2a --- /dev/null +++ b/dash_shop/lib/screens/product_detail_screen.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../view_models/cart_view_model.dart'; +import '../view_models/catalog_view_model.dart'; +import '../widgets/cart_fab.dart'; + +class ProductDetailScreen extends StatelessWidget { + final String productId; + final CatalogViewModel viewModel; + final CartViewModel cartViewModel; + + const ProductDetailScreen({ + super.key, + required this.productId, + required this.viewModel, + required this.cartViewModel, + }); + + @override + Widget build(BuildContext context) { + final product = viewModel.getProductById(productId); + + if (product == null) { + return Scaffold( + appBar: AppBar(title: const Text('Product Not Found')), + body: const Center(child: Text('Product not found')), + ); + } + + return Scaffold( + backgroundColor: Colors.white, + floatingActionButton: CartFab(viewModel: cartViewModel), + body: CustomScrollView( + slivers: [ + SliverAppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + pinned: true, + expandedHeight: 300, + flexibleSpace: FlexibleSpaceBar( + background: Image.asset( + product.imageUrl, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + const Icon(Icons.image, size: 100, color: Colors.grey), + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(24), + sliver: SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: GoogleFonts.playfairDisplay( + fontSize: 32, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + color: Colors.black, + ), + ), + const SizedBox(height: 8), + Text( + '\$${product.price.toStringAsFixed(2)}', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + const SizedBox(height: 24), + Text( + product.description, + style: const TextStyle( + fontSize: 16, + color: Colors.black87, + height: 1.5, + ), + ), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + cartViewModel.addToCart(product); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Add to Cart', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/dash_shop/lib/screens/profile_screen.dart b/dash_shop/lib/screens/profile_screen.dart new file mode 100644 index 0000000..d652d34 --- /dev/null +++ b/dash_shop/lib/screens/profile_screen.dart @@ -0,0 +1,447 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../view_models/auth_view_model.dart'; +import '../models/user.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ProfileScreen extends StatefulWidget { + final AuthViewModel viewModel; + + const ProfileScreen({super.key, required this.viewModel}); + + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + bool _notificationsEnabled = true; + bool _biometricsEnabled = false; + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: widget.viewModel, + builder: (context, _) { + final user = widget.viewModel.user; + return Scaffold( + backgroundColor: Colors.white, + body: + user == null + ? _buildSignedOutView(context) + : _buildSignedInView(context, user), + ); + }, + ); + } + + void _confirmSignOut(BuildContext context) { + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('Sign Out?'), + content: const Text( + 'Are you sure you want to sign out of The Dash Shop?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + widget.viewModel.signOut(); + Navigator.pop(context); + }, + child: const Text( + 'Sign Out', + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ); + } + + Widget _buildSignedOutView(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Dash Shop', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + const SizedBox(height: 48), + ElevatedButton( + onPressed: + () => widget.viewModel.signIn('Dash User', 'dash@example.com'), + style: ElevatedButton.styleFrom(minimumSize: const Size(200, 48)), + child: const Text('Sign In to Account'), + ), + ], + ), + ); + } + + Widget _buildSignedInView(BuildContext context, User user) { + final primaryColor = Theme.of(context).colorScheme.primary; + + return CustomScrollView( + slivers: [ + SliverAppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + pinned: true, + expandedHeight: 120, + flexibleSpace: FlexibleSpaceBar( + centerTitle: false, + titlePadding: const EdgeInsetsDirectional.only( + start: 24, + bottom: 12, + ), + title: Text( + 'Profile', + style: GoogleFonts.playfairDisplay( + fontSize: 32, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + color: Colors.black, + ), + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.logout, color: Colors.black, size: 20), + onPressed: () => _confirmSignOut(context), + ), + const SizedBox(width: 8), + ], + ), + SliverToBoxAdapter( + child: Column( + children: [ + const SizedBox(height: 20), + Text( + user.name, + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w600, + color: primaryColor, + ), + ), + const SizedBox(height: 24), + _buildProfileAvatar(context), + const SizedBox(height: 40), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildStatItem( + context, + 'Orders', + '12', + () => _showOrders(context), + ), + const SizedBox(width: 48), + _buildStatItem( + context, + 'Points', + '${user.dashPoints}', + () => _showPoints(context), + ), + ], + ), + const SizedBox(height: 60), + _buildActionList(context, primaryColor), + const SizedBox(height: 40), + ], + ), + ), + ], + ); + } + + Widget _buildProfileAvatar(BuildContext context) { + return InkWell( + onTap: () => _updateAvatar(context), + borderRadius: BorderRadius.circular(75), + child: Container( + width: 150, + height: 150, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: const DecorationImage( + image: AssetImage('assets/images/dash.png'), + fit: BoxFit.cover, + ), + border: Border.all(color: Colors.grey.shade100, width: 1), + ), + ), + ); + } + + void _updateAvatar(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (context) => SafeArea( + child: Wrap( + children: [ + ListTile( + leading: const Icon(Icons.photo_library), + title: const Text('Photo Library'), + onTap: () => Navigator.pop(context), + ), + ListTile( + leading: const Icon(Icons.camera_alt), + title: const Text('Camera'), + onTap: () => Navigator.pop(context), + ), + ], + ), + ), + ); + } + + Widget _buildStatItem( + BuildContext context, + String label, + String value, + VoidCallback onTap, + ) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ], + ), + ), + ); + } + + void _showOrders(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + height: MediaQuery.of(context).size.height * 0.7, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(16), + child: Text( + 'Recent Orders', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + const Divider(), + Expanded( + child: ListView.builder( + itemCount: 5, + itemBuilder: (context, i) => ListTile( + leading: const Icon(Icons.shopping_bag), + title: Text('Order #D-10${5 - i}'), + subtitle: const Text('Status: Delivered'), + trailing: const Text('\$45.00'), + ), + ), + ), + ], + ), + ), + ); + } + + void _showPoints(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Dash Rewards'), + content: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.stars, size: 60, color: Colors.amber), + SizedBox(height: 16), + Text( + 'You have 750 Dash Points!', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Text('Keep shopping to earn more rewards.'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Great!'), + ), + ], + ), + ); + } + + Widget _buildActionList(BuildContext context, Color primaryColor) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildActionTile( + context, + title: 'Notifications', + subtitle: 'Push notifications', + trailing: Switch( + value: _notificationsEnabled, + activeTrackColor: const Color(0xFF4CD964), + onChanged: (v) => setState(() => _notificationsEnabled = v), + ), + ), + _buildActionTile( + context, + title: 'Security', + subtitle: 'Biometric Login', + trailing: Switch( + value: _biometricsEnabled, + activeTrackColor: const Color(0xFF4CD964), + onChanged: (v) => setState(() => _biometricsEnabled = v), + ), + ), + _buildActionTile( + context, + title: 'Support', + subtitle: 'flutter.dev', + onTap: () => _launchURL('https://flutter.dev'), + ), + _buildActionTile( + context, + title: 'Birthdate', + subtitle: 'October 15, 1995', + onTap: () => _selectDate(context), + ), + _buildActionTile( + context, + title: 'Location', + subtitle: 'San Francisco, CA', + onTap: () => _selectLocation(context), + ), + ], + ), + ); + } + + void _launchURL(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } + } + + void _selectDate(BuildContext context) async { + await showDatePicker( + context: context, + initialDate: DateTime(1995, 10, 15), + firstDate: DateTime(1900), + lastDate: DateTime.now(), + ); + } + + void _selectLocation(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (context) => Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Current Location', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text('San Francisco, California, USA'), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text('Confirm'), + ), + ], + ), + ), + ); + } + + Widget _buildActionTile( + BuildContext context, { + required String title, + required String subtitle, + Widget? trailing, + VoidCallback? onTap, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 32), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + Text( + subtitle, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + if (trailing != null) + trailing + else + const Icon(Icons.chevron_right, size: 20, color: Colors.grey), + ], + ), + ), + ); + } +} diff --git a/dash_shop/lib/services/auth_api_service.dart b/dash_shop/lib/services/auth_api_service.dart new file mode 100644 index 0000000..1b80b69 --- /dev/null +++ b/dash_shop/lib/services/auth_api_service.dart @@ -0,0 +1,31 @@ +class AuthApiService { + Future> fetchUserRaw(String name, String email) async { + // Simulate network delay + await Future.delayed(const Duration(milliseconds: 500)); + + return { + 'id': '1', + 'name': name, + 'email': email, + 'joinDate': DateTime( + 2023, + 10, + 15, + ).toIso8601String(), // Use ISO 8601 string to represent data as standard raw map + 'dashPoints': 750, + }; + } + + Future> fetchCurrentUserRaw() async { + // Simulate network delay + await Future.delayed(const Duration(milliseconds: 500)); + + return { + 'id': '1', + 'name': 'Dash', + 'email': 'dash@example.com', + 'joinDate': DateTime(2023, 10, 15).toIso8601String(), + 'dashPoints': 750, + }; + } +} diff --git a/dash_shop/lib/services/catalog_api_service.dart b/dash_shop/lib/services/catalog_api_service.dart new file mode 100644 index 0000000..74913ba --- /dev/null +++ b/dash_shop/lib/services/catalog_api_service.dart @@ -0,0 +1,66 @@ +class CatalogApiService { + Future>> fetchProductsRaw() async { + // Simulate network delay + await Future.delayed(const Duration(milliseconds: 500)); + + return [ + { + 'id': 'dash-plush', + 'name': 'Dash Plushie', + 'description': 'A soft and cuddly Dash plushie.', + 'price': 19.99, + 'imageUrl': 'assets/images/products/dash-plush.png', + 'category': 'Plushies', + }, + { + 'id': 'dash-phone-case', + 'name': 'Dash Phone Case', + 'description': + 'Sleek and protective phone case with a gorgeous Dash pattern.', + 'price': 24.99, + 'imageUrl': 'assets/images/products/dash-phone-case.png', + 'category': 'Accessories', + }, + { + 'id': 'dash-keyboard', + 'name': 'Dash Keyboard', + 'description': 'Mechanical keyboard with Dash-themed keycaps.', + 'price': 89.99, + 'imageUrl': 'assets/images/products/dash-keyboard.png', + 'category': 'Keyboards', + }, + { + 'id': 'dash-hoodie', + 'name': 'Dash Hoodie', + 'description': 'Warm and cozy hoodie featuring Dash.', + 'price': 45.00, + 'imageUrl': 'assets/images/products/dash-hoodie.png', + 'category': 'Apparel', + }, + { + 'id': 'dash-stickers', + 'name': 'Dash Stickers', + 'description': 'A pack of high-quality Dash stickers.', + 'price': 5.00, + 'imageUrl': 'assets/images/products/dash-stickers.png', + 'category': 'Stickers', + }, + { + 'id': 'dash-mug', + 'name': 'Dash Mug', + 'description': 'Start your morning with Dash.', + 'price': 15.00, + 'imageUrl': 'assets/images/products/dash-mug.png', + 'category': 'Home', + }, + { + 'id': 'dash-backpack', + 'name': 'Dash Backpack', + 'description': 'Spacious backpack for all your gear.', + 'price': 65.00, + 'imageUrl': 'assets/images/products/dash-backpack.png', + 'category': 'Apparel', + }, + ]; + } +} diff --git a/dash_shop/lib/utils/formatters.dart b/dash_shop/lib/utils/formatters.dart new file mode 100644 index 0000000..b1e01a8 --- /dev/null +++ b/dash_shop/lib/utils/formatters.dart @@ -0,0 +1,103 @@ +import 'package:flutter/services.dart'; + +class CardNumberFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + var text = newValue.text.replaceAll(' ', ''); + if (text.length > 16) { + return oldValue; + } + + var buffer = StringBuffer(); + for (int i = 0; i < text.length; i++) { + buffer.write(text[i]); + var nonSpaceLength = i + 1; + if (nonSpaceLength % 4 == 0 && + nonSpaceLength != 0 && + nonSpaceLength != text.length) { + buffer.write(' '); + } + } + + var string = buffer.toString(); + return newValue.copyWith( + text: string, + selection: TextSelection.collapsed(offset: string.length), + ); + } +} + +class ExpirationDateFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + var text = newValue.text.replaceAll('/', ''); + if (text.length > 4) { + return oldValue; + } + + var buffer = StringBuffer(); + for (int i = 0; i < text.length; i++) { + buffer.write(text[i]); + var nonSlashLength = i + 1; + if (nonSlashLength == 2 && nonSlashLength != text.length) { + buffer.write('/'); + } + } + + var string = buffer.toString(); + return newValue.copyWith( + text: string, + selection: TextSelection.collapsed(offset: string.length), + ); + } +} + +class CVVFormatter extends TextInputFormatter { + final int maxLength; + + CVVFormatter({this.maxLength = 4}); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (newValue.text.length > maxLength) { + return oldValue; + } + return newValue; + } +} + +class PhoneNumberFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + var text = newValue.text.replaceAll(RegExp(r'\D'), ''); + if (text.length > 10) { + text = text.substring(0, 10); + } + + var buffer = StringBuffer(); + for (int i = 0; i < text.length; i++) { + if (i == 0) buffer.write('('); + buffer.write(text[i]); + if (i == 2 && i != text.length - 1) buffer.write(') '); + if (i == 5 && i != text.length - 1) buffer.write('-'); + } + + var string = buffer.toString(); + return TextEditingValue( + text: string, + selection: TextSelection.collapsed(offset: string.length), + ); + } +} diff --git a/dash_shop/lib/view_models/auth_view_model.dart b/dash_shop/lib/view_models/auth_view_model.dart new file mode 100644 index 0000000..2bbcf5d --- /dev/null +++ b/dash_shop/lib/view_models/auth_view_model.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import '../models/user.dart'; +import '../repositories/auth_repository.dart'; + +class AuthViewModel extends ChangeNotifier { + final AuthRepository _authRepository; + bool _isLoading = false; + String? _error; + + AuthViewModel(this._authRepository) { + _init(); + } + + User? get user => _authRepository.currentUser; + bool get isAuthenticated => _authRepository.isAuthenticated; + bool get isLoading => _isLoading; + String? get error => _error; + + Future _init() async { + _setLoading(true); + await _authRepository.checkAuthStatus(); + _setLoading(false); + } + + Future signIn(String name, String email) async { + _setLoading(true); + try { + await _authRepository.signIn(name, email); + } catch (e) { + _error = e.toString(); + } finally { + _setLoading(false); + } + } + + void signOut() { + _authRepository.signOut(); + notifyListeners(); + } + + void _setLoading(bool value) { + _isLoading = value; + notifyListeners(); + } +} diff --git a/dash_shop/lib/view_models/cart_view_model.dart b/dash_shop/lib/view_models/cart_view_model.dart new file mode 100644 index 0000000..347d582 --- /dev/null +++ b/dash_shop/lib/view_models/cart_view_model.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import '../models/product.dart'; +import '../models/cart_item.dart'; +import '../repositories/cart_repository.dart'; + +class CartViewModel extends ChangeNotifier { + final CartRepository _cartRepository; + + CartViewModel(this._cartRepository) { + _cartRepository.addListener(_onCartChanged); + } + + void _onCartChanged() { + notifyListeners(); + } + + @override + void dispose() { + _cartRepository.removeListener(_onCartChanged); + super.dispose(); + } + + Map get items => _cartRepository.items; + double get subtotal => _cartRepository.subtotal; + int get itemCount => _cartRepository.itemCount; + + void addToCart(Product product) { + _cartRepository.addToCart(product); + } + + void removeFromCart(String productId) { + _cartRepository.removeFromCart(productId); + } + + void clear() { + _cartRepository.clear(); + } +} diff --git a/dash_shop/lib/view_models/catalog_view_model.dart b/dash_shop/lib/view_models/catalog_view_model.dart new file mode 100644 index 0000000..66db3ca --- /dev/null +++ b/dash_shop/lib/view_models/catalog_view_model.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import '../models/product.dart'; +import '../repositories/catalog_repository.dart'; + +class CatalogViewModel extends ChangeNotifier { + final CatalogRepository _catalogRepository; + + List _products = []; + bool _isLoading = false; + String? _error; + + CatalogViewModel(this._catalogRepository) { + loadProducts(); + } + + List get products => List.unmodifiable(_products); + bool get isLoading => _isLoading; + String? get error => _error; + + Future loadProducts() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _products = await _catalogRepository.getProducts(); + } catch (e) { + _error = e.toString(); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Product? getProductById(String id) { + try { + return _products.firstWhere((p) => p.id == id); + } catch (_) { + return null; + } + } +} diff --git a/dash_shop/lib/widgets/cart_fab.dart b/dash_shop/lib/widgets/cart_fab.dart new file mode 100644 index 0000000..8c5acf3 --- /dev/null +++ b/dash_shop/lib/widgets/cart_fab.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../view_models/cart_view_model.dart'; + +class CartFab extends StatelessWidget { + final CartViewModel viewModel; + + const CartFab({super.key, required this.viewModel}); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + final hasItems = viewModel.itemCount > 0; + final subtotalText = '\$${viewModel.subtotal.toStringAsFixed(2)}'; + + return Material( + color: Colors.black, + borderRadius: BorderRadius.circular(28), + elevation: 6, + child: InkWell( + key: const ValueKey('cart_fab'), + borderRadius: BorderRadius.circular(28), + onTap: () => context.go('/cart'), + child: Container( + height: 56, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.shopping_cart_outlined, color: Colors.white), + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + child: hasItems + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 8), + Text( + subtotalText, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ) + : const SizedBox.shrink(), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/dash_shop/lib/widgets/dash_button.dart b/dash_shop/lib/widgets/dash_button.dart new file mode 100644 index 0000000..8c45a64 --- /dev/null +++ b/dash_shop/lib/widgets/dash_button.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class DashButton extends StatelessWidget { + final String label; + final VoidCallback? onPressed; + final bool isPrimary; + + const DashButton({ + super.key, + required this.label, + this.onPressed, + this.isPrimary = true, + }); + + @override + Widget build(BuildContext context) { + if (isPrimary) { + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + ), + child: Text(label), + ); + } else { + return OutlinedButton(onPressed: onPressed, child: Text(label)); + } + } +} diff --git a/dash_shop/lib/widgets/product_card.dart b/dash_shop/lib/widgets/product_card.dart new file mode 100644 index 0000000..451bcc8 --- /dev/null +++ b/dash_shop/lib/widgets/product_card.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import '../models/product.dart'; + +class ProductCard extends StatelessWidget { + final Product product; + final int quantity; + final VoidCallback onAdd; + final VoidCallback onRemove; + final VoidCallback onTap; + + const ProductCard({ + super.key, + required this.product, + this.quantity = 0, + required this.onAdd, + required this.onRemove, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + key: ValueKey('product_card_${product.id}'), + onTap: onTap, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color( + 0xFFF1F4F8, + ), // light gray from original code works well + borderRadius: BorderRadius.circular(24), + ), + child: Stack( + fit: StackFit.expand, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Image.asset( + product.imageUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Icon( + product.name.contains('Keyboard') + ? Icons.keyboard + : product.name.contains('Plushie') + ? Icons.toys + : product.name.contains('Hoodie') + ? Icons.checkroom + : Icons.shopping_bag, + size: 48, + color: Colors.grey.shade400, + ), + ); + }, + ), + ), + Positioned( + bottom: 12, + right: 12, + child: _buildQuantityControl(), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + Text( + product.name, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + '\$${product.price.toStringAsFixed(2)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.black, + ), + ), + ], + ), + ); + } + + Widget _buildQuantityControl() { + if (quantity == 0) { + return Material( + color: Colors.white, + shape: const CircleBorder(), + elevation: 2, + child: InkWell( + key: ValueKey('product_add_${product.id}'), + onTap: onAdd, + customBorder: const CircleBorder(), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.add, size: 24, color: Colors.black), + ), + ), + ); + } + + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + elevation: 2, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + key: ValueKey('product_remove_${product.id}'), + onTap: onRemove, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.fromLTRB(12, 8, 8, 8), + child: Icon(Icons.remove, size: 20, color: Colors.black), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + '$quantity', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + ), + InkWell( + key: ValueKey('product_add_${product.id}'), + onTap: onAdd, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.fromLTRB(8, 8, 12, 8), + child: Icon(Icons.add, size: 20, color: Colors.black), + ), + ), + ], + ), + ); + } +} diff --git a/dash_shop/pubspec.yaml b/dash_shop/pubspec.yaml new file mode 100644 index 0000000..091ea07 --- /dev/null +++ b/dash_shop/pubspec.yaml @@ -0,0 +1,38 @@ +name: dash_shop +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0+1 + +environment: + sdk: ^3.11.0 + +dependencies: + flutter: + sdk: flutter + go_router: ^17.1.0 + logging: ^1.3.0 + url_launcher: ^6.3.2 + http: ^1.1.0 + google_fonts: ^8.0.2 + pay: ^3.3.0 + flutter_driver: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + args: ^2.7.0 + integration_test: + sdk: flutter + flutter_driver: + sdk: flutter + checks: ^0.3.1 + test: ^1.24.0 + +flutter: + uses-material-design: true + assets: + - assets/images/ + - assets/images/products/ + - assets/payment_configurations/ diff --git a/dash_shop/test/cart_view_model_test.dart b/dash_shop/test/cart_view_model_test.dart new file mode 100644 index 0000000..25e73a3 --- /dev/null +++ b/dash_shop/test/cart_view_model_test.dart @@ -0,0 +1,28 @@ +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:dash_shop/view_models/cart_view_model.dart'; +import 'package:dash_shop/repositories/cart_repository.dart'; +import 'package:dash_shop/models/product.dart'; + +void main() { + group('CartViewModel', () { + final product = Product( + id: '1', + name: 'Test Product', + description: 'Test Description', + price: 10.0, + imageUrl: 'test.png', + category: 'Test Category', + ); + + test('clear() removes all items from the cart', () { + final viewModel = CartViewModel(CartRepository()); + viewModel.addToCart(product); + check(viewModel.itemCount).equals(1); + + viewModel.clear(); + check(viewModel.itemCount).equals(0); + check(viewModel.items).isEmpty(); + }); + }); +} diff --git a/dash_shop/test/formatters_test.dart b/dash_shop/test/formatters_test.dart new file mode 100644 index 0000000..f0f8f7b --- /dev/null +++ b/dash_shop/test/formatters_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:dash_shop/utils/formatters.dart'; + +void main() { + group('CardNumberFormatter', () { + final formatter = CardNumberFormatter(); + + test('adds spaces every 4 digits', () { + const oldValue = TextEditingValue.empty; + const newValue = TextEditingValue(text: '12345'); + final result = formatter.formatEditUpdate(oldValue, newValue); + expect(result.text, '1234 5'); + }); + + test('limits to 16 digits', () { + const oldValue = TextEditingValue(text: '1234 1234 1234 1234'); + const newValue = TextEditingValue(text: '1234 1234 1234 12345'); + final result = formatter.formatEditUpdate(oldValue, newValue); + expect(result.text, '1234 1234 1234 1234'); + }); + }); + + group('ExpirationDateFormatter', () { + final formatter = ExpirationDateFormatter(); + + test('adds slash after 2 digits', () { + const oldValue = TextEditingValue.empty; + const newValue = TextEditingValue(text: '123'); + final result = formatter.formatEditUpdate(oldValue, newValue); + expect(result.text, '12/3'); + }); + + test('limits to 4 digits', () { + const oldValue = TextEditingValue(text: '12/34'); + const newValue = TextEditingValue(text: '12/345'); + final result = formatter.formatEditUpdate(oldValue, newValue); + expect(result.text, '12/34'); + }); + }); + + group('PhoneNumberFormatter', () { + final formatter = PhoneNumberFormatter(); + + test('formats phone number correctly', () { + const oldValue = TextEditingValue.empty; + const newValue = TextEditingValue(text: '1234567890'); + final result = formatter.formatEditUpdate(oldValue, newValue); + expect(result.text, '(123) 456-7890'); + }); + + test('no trailing delimiters', () { + const oldValue = TextEditingValue.empty; + const newValue = TextEditingValue(text: '123'); + final result = formatter.formatEditUpdate(oldValue, newValue); + expect(result.text, '(123'); // No trailing ") " + }); + + test('backspace after hyphen allows deletion', () { + // User typed 7 digits, got (123) 456-7 + const oldValue = TextEditingValue(text: '(123) 456-7'); + // User backspaces the '7' + const newValue = TextEditingValue(text: '(123) 456-'); + final result = formatter.formatEditUpdate(oldValue, newValue); + expect(result.text, '(123) 456'); // Delimiter is automatically removed + }); + }); +} diff --git a/dash_shop/test/widgets/cart_fab_test.dart b/dash_shop/test/widgets/cart_fab_test.dart new file mode 100644 index 0000000..e0489f4 --- /dev/null +++ b/dash_shop/test/widgets/cart_fab_test.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:checks/checks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:dash_shop/widgets/cart_fab.dart'; +import 'package:dash_shop/view_models/cart_view_model.dart'; +import 'package:dash_shop/repositories/cart_repository.dart'; +import 'package:dash_shop/models/product.dart'; + +void main() { + group('CartFab Widget Test', () { + late CartViewModel cartViewModel; + late Product product; + + setUp(() { + cartViewModel = CartViewModel(CartRepository()); + product = const Product( + id: '1', + name: 'Test Product', + description: 'A product for testing', + price: 19.99, + imageUrl: 'test.png', + category: 'Test Category', + ); + }); + + Widget createFabUnderTest(GoRouter router) { + return MaterialApp.router( + routerConfig: router, + ); + } + + testWidgets('renders only icon when cart is empty', (WidgetTester tester) async { + final router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (context, state) => Scaffold(body: CartFab(viewModel: cartViewModel)), + ), + ], + ); + + await tester.pumpWidget(createFabUnderTest(router)); + + expect(find.byIcon(Icons.shopping_cart_outlined), findsOneWidget); + expect(find.text('\$19.99'), findsNothing); + }); + + testWidgets('expands and shows subtotal when items are added', (WidgetTester tester) async { + final router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (context, state) => Scaffold(body: CartFab(viewModel: cartViewModel)), + ), + ], + ); + + await tester.pumpWidget(createFabUnderTest(router)); + + // Add item to cart + cartViewModel.addToCart(product); + + // Pump frames for ListenableBuilder / AnimatedSize animation + await tester.pump(); + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.text('\$19.99'), findsOneWidget); + }); + + testWidgets('navigates to /cart when tapped', (WidgetTester tester) async { + bool pushedCart = false; + final router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (context, state) => Scaffold(body: CartFab(viewModel: cartViewModel)), + ), + GoRoute( + path: '/cart', + builder: (context, state) { + pushedCart = true; + return const Scaffold(body: Text('Cart Screen')); + }, + ), + ], + ); + + await tester.pumpWidget(createFabUnderTest(router)); + + await tester.tap(find.byType(CartFab)); + await tester.pumpAndSettle(); + + check(pushedCart).isTrue(); + expect(find.text('Cart Screen'), findsOneWidget); + }); + }); +} diff --git a/dash_shop/test/widgets/dash_button_test.dart b/dash_shop/test/widgets/dash_button_test.dart new file mode 100644 index 0000000..9dfb99c --- /dev/null +++ b/dash_shop/test/widgets/dash_button_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:dash_shop/widgets/dash_button.dart'; +import 'package:checks/checks.dart'; + +void main() { + group('DashButton Widget Test', () { + testWidgets('renders ElevatedButton when isPrimary is true', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DashButton( + label: 'Primary Button', + isPrimary: true, + onPressed: () {}, + ), + ), + ), + ); + + expect(find.text('Primary Button'), findsOneWidget); + expect(find.byType(ElevatedButton), findsOneWidget); + expect(find.byType(OutlinedButton), findsNothing); + }); + + testWidgets('renders OutlinedButton when isPrimary is false', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DashButton( + label: 'Secondary Button', + isPrimary: false, + onPressed: () {}, + ), + ), + ), + ); + + expect(find.text('Secondary Button'), findsOneWidget); + expect(find.byType(OutlinedButton), findsOneWidget); + expect(find.byType(ElevatedButton), findsNothing); + }); + + testWidgets('calls onPressed when tapped', (WidgetTester tester) async { + bool tapped = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DashButton( + label: 'Tap Me', + onPressed: () { + tapped = true; + }, + ), + ), + ), + ); + + await tester.tap(find.text('Tap Me')); + await tester.pumpAndSettle(); + + check(tapped).isTrue(); + }); + }); +} diff --git a/demo b/demo new file mode 100644 index 0000000..de4c8fe --- /dev/null +++ b/demo @@ -0,0 +1,107 @@ +ANY PROGRAM IS PROVIDED UNDER THE TERMS ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM, PURCHASE OR SALE IS ACCEPTANCE OF THIS AGREEMENT. + +0. DEFINITIONS + +"Contribution" means: Published and shared publicly with knowledge of reading and writing skills to understand this content. + +0) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and + +0) in the case of each subsequent Contributor: + +0) changes to the Program, and + +0) additions to the Program; + +where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives, watches, or hears could be at fault the Program under this Agreement, including all Contributors can be prosecuted by at United States government. + +0. GRANT OF RIGHTS + +0) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. + +0) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. + +0) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. + +0) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. + +0. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: + +0) it complies with the terms and conditions of this Agreement; and + +0) its license agreement: + +0) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; + +0) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; + +0) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and + +0) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + +0) it must be made available under this Agreement; and + +0) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the Program. + +Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. + +0. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. + +For Sample, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. + +0. NO WARRANTY ON EXAMPLE + +SET FORTH IN THIS AGREEMENT, THE OWNER IS PROVIDED ON A SERVICE FOR THE PUBLIC "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. + +0. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES BY USING LLC, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE BEING ARRESTED FOR PROPERTY DAMAGES ARISING IN ANY WAY OUT OF THE USE OR CARELESS ACTS OR EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +0. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. + + + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION FOR MARKETPLACE AGREEMENT BELOW + +1. Marketplace Agreement. + +"The Unlicense" shall mean the terms and conditions for use, reproduction, and distribution as described line by line 1 through 107 of this document. + +"Trademark" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"worker" (or "coworker") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"reSource" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Samsung thing's" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"WORKGROUP" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"PUBLIC WORKS" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"EMPLOYEES" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or diff --git a/devexp_demos/devtools_companion/.gitignore b/devexp_demos/devtools_companion/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/devexp_demos/devtools_companion/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/devexp_demos/devtools_companion/.metadata b/devexp_demos/devtools_companion/.metadata new file mode 100644 index 0000000..6cb6771 --- /dev/null +++ b/devexp_demos/devtools_companion/.metadata @@ -0,0 +1,45 @@ +# 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: "bd24c21d833b4dd046cd5d2a3440c030145cbbbd" + channel: "[user-branch]" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + base_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + - platform: android + create_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + base_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + - platform: ios + create_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + base_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + - platform: linux + create_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + base_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + - platform: macos + create_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + base_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + - platform: web + create_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + base_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + - platform: windows + create_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + base_revision: bd24c21d833b4dd046cd5d2a3440c030145cbbbd + + # 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' diff --git a/devexp_demos/devtools_companion/README.md b/devexp_demos/devtools_companion/README.md new file mode 100644 index 0000000..bee1331 --- /dev/null +++ b/devexp_demos/devtools_companion/README.md @@ -0,0 +1,9 @@ +# Welcome to the DevTools Companion App! + +This app is intended to be used by contributors to Dart & Flutter DevTools. + +## Instructions for use: + +1. Connect DevTools to this app. +2. Use the sidebar to navigate to the companion screen for the panel you are working on in DevTools. +3. ...that's it! \ No newline at end of file diff --git a/devexp_demos/devtools_companion/analysis_options.yaml b/devexp_demos/devtools_companion/analysis_options.yaml new file mode 100644 index 0000000..4c19418 --- /dev/null +++ b/devexp_demos/devtools_companion/analysis_options.yaml @@ -0,0 +1,83 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + - always_declare_return_types + - annotate_overrides + - avoid_classes_with_only_static_members + - avoid_empty_else + - avoid_dynamic_calls + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_print + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_slow_async_io + - avoid_unnecessary_containers + - await_only_futures + - camel_case_types + - cancel_subscriptions + - comment_references + - control_flow_in_finally + - directives_ordering + - discarded_futures + - empty_catches + - empty_constructor_bodies + - empty_statements + - hash_and_equals + - implementation_imports + - library_names + - library_prefixes + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - overridden_fields + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_contains + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_foreach + - prefer_initializing_formals + - prefer_is_empty + - prefer_is_not_empty + - prefer_relative_imports + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_async + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_library_directive + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_rethrow_when_possible + - use_string_in_part_of_directives + - valid_regexps \ No newline at end of file diff --git a/devexp_demos/devtools_companion/android/.gitignore b/devexp_demos/devtools_companion/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/devexp_demos/devtools_companion/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/devexp_demos/devtools_companion/android/app/build.gradle.kts b/devexp_demos/devtools_companion/android/app/build.gradle.kts new file mode 100644 index 0000000..3784b97 --- /dev/null +++ b/devexp_demos/devtools_companion/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +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") +} + +android { + namespace = "com.example.devtools_companion" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.devtools_companion" + // 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 + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/devexp_demos/devtools_companion/android/app/src/debug/AndroidManifest.xml b/devexp_demos/devtools_companion/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/devexp_demos/devtools_companion/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/devexp_demos/devtools_companion/android/app/src/main/AndroidManifest.xml b/devexp_demos/devtools_companion/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..abfe3fa --- /dev/null +++ b/devexp_demos/devtools_companion/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/devexp_demos/devtools_companion/android/app/src/main/kotlin/com/example/devtools_companion/MainActivity.kt b/devexp_demos/devtools_companion/android/app/src/main/kotlin/com/example/devtools_companion/MainActivity.kt new file mode 100644 index 0000000..cb2e056 --- /dev/null +++ b/devexp_demos/devtools_companion/android/app/src/main/kotlin/com/example/devtools_companion/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.devtools_companion + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/devexp_demos/devtools_companion/android/app/src/main/res/drawable-v21/launch_background.xml b/devexp_demos/devtools_companion/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/devexp_demos/devtools_companion/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/devexp_demos/devtools_companion/android/app/src/main/res/drawable/launch_background.xml b/devexp_demos/devtools_companion/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/devexp_demos/devtools_companion/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/devexp_demos/devtools_companion/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/devexp_demos/devtools_companion/android/app/src/main/res/values-night/styles.xml b/devexp_demos/devtools_companion/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/devexp_demos/devtools_companion/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/devexp_demos/devtools_companion/android/app/src/main/res/values/styles.xml b/devexp_demos/devtools_companion/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/devexp_demos/devtools_companion/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/devexp_demos/devtools_companion/android/app/src/profile/AndroidManifest.xml b/devexp_demos/devtools_companion/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/devexp_demos/devtools_companion/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/devexp_demos/devtools_companion/android/build.gradle.kts b/devexp_demos/devtools_companion/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/devexp_demos/devtools_companion/android/build.gradle.kts @@ -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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/devexp_demos/devtools_companion/android/gradle.properties b/devexp_demos/devtools_companion/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/devexp_demos/devtools_companion/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/devexp_demos/devtools_companion/android/gradle/wrapper/gradle-wrapper.properties b/devexp_demos/devtools_companion/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/devexp_demos/devtools_companion/android/gradle/wrapper/gradle-wrapper.properties @@ -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-all.zip diff --git a/devexp_demos/devtools_companion/android/settings.gradle.kts b/devexp_demos/devtools_companion/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/devexp_demos/devtools_companion/android/settings.gradle.kts @@ -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.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto/LICENSE.txt b/devexp_demos/devtools_companion/assets/fonts/Roboto/LICENSE.txt new file mode 100755 index 0000000..d645695 --- /dev/null +++ b/devexp_demos/devtools_companion/assets/fonts/Roboto/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Black.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Black.ttf new file mode 100755 index 0000000..689fe5c Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Black.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Bold.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Bold.ttf new file mode 100755 index 0000000..d3f01ad Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Bold.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Light.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Light.ttf new file mode 100755 index 0000000..219063a Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Light.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Medium.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Medium.ttf new file mode 100755 index 0000000..1a7f3b0 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Medium.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Regular.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Regular.ttf new file mode 100755 index 0000000..2c97eea Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Regular.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Thin.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Thin.ttf new file mode 100755 index 0000000..b74a4fd Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto/Roboto-Thin.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/LICENSE.txt b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Bold.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Bold.ttf new file mode 100644 index 0000000..482f028 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Bold.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Light.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Light.ttf new file mode 100644 index 0000000..3c845d4 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Light.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Medium.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Medium.ttf new file mode 100644 index 0000000..c496725 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Medium.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Regular.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Regular.ttf new file mode 100644 index 0000000..5919b5d Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Regular.ttf differ diff --git a/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Thin.ttf b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Thin.ttf new file mode 100644 index 0000000..65bf26a Binary files /dev/null and b/devexp_demos/devtools_companion/assets/fonts/Roboto_Mono/RobotoMono-Thin.ttf differ diff --git a/devexp_demos/devtools_companion/assets/icons/app_size.png b/devexp_demos/devtools_companion/assets/icons/app_size.png new file mode 100644 index 0000000..9019605 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/app_size.png differ diff --git a/devexp_demos/devtools_companion/assets/icons/cpu_profiler.png b/devexp_demos/devtools_companion/assets/icons/cpu_profiler.png new file mode 100644 index 0000000..4cbbbe4 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/cpu_profiler.png differ diff --git a/devexp_demos/devtools_companion/assets/icons/debugger.png b/devexp_demos/devtools_companion/assets/icons/debugger.png new file mode 100644 index 0000000..cc73dc1 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/debugger.png differ diff --git a/devexp_demos/devtools_companion/assets/icons/deep_links.png b/devexp_demos/devtools_companion/assets/icons/deep_links.png new file mode 100644 index 0000000..74eb1c9 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/deep_links.png differ diff --git a/devexp_demos/devtools_companion/assets/icons/devtools.png b/devexp_demos/devtools_companion/assets/icons/devtools.png new file mode 100644 index 0000000..a3de7b9 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/devtools.png differ diff --git a/devexp_demos/devtools_companion/assets/icons/inspector.png b/devexp_demos/devtools_companion/assets/icons/inspector.png new file mode 100644 index 0000000..c5dee4b Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/inspector.png differ diff --git a/devexp_demos/devtools_companion/assets/icons/logging.png b/devexp_demos/devtools_companion/assets/icons/logging.png new file mode 100644 index 0000000..b64d206 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/logging.png differ diff --git a/devexp_demos/devtools_companion/assets/icons/memory.png b/devexp_demos/devtools_companion/assets/icons/memory.png new file mode 100644 index 0000000..91f7527 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/memory.png differ diff --git a/devexp_demos/devtools_companion/assets/icons/network.png b/devexp_demos/devtools_companion/assets/icons/network.png new file mode 100644 index 0000000..30ed61d Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/network.png differ diff --git a/devexp_demos/devtools_companion/assets/icons/performance.png b/devexp_demos/devtools_companion/assets/icons/performance.png new file mode 100644 index 0000000..52697f3 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/icons/performance.png differ diff --git a/devexp_demos/devtools_companion/assets/images/apple.jpg b/devexp_demos/devtools_companion/assets/images/apple.jpg new file mode 100644 index 0000000..7305880 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/apple.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/artichoke.jpg b/devexp_demos/devtools_companion/assets/images/artichoke.jpg new file mode 100644 index 0000000..43337dd Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/artichoke.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/asparagus.jpg b/devexp_demos/devtools_companion/assets/images/asparagus.jpg new file mode 100644 index 0000000..76d340e Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/asparagus.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/avocado.jpg b/devexp_demos/devtools_companion/assets/images/avocado.jpg new file mode 100644 index 0000000..16ec1e7 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/avocado.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/blackberry.jpg b/devexp_demos/devtools_companion/assets/images/blackberry.jpg new file mode 100644 index 0000000..4d8193a Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/blackberry.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/cantaloupe.jpg b/devexp_demos/devtools_companion/assets/images/cantaloupe.jpg new file mode 100644 index 0000000..f048570 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/cantaloupe.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/cauliflower.jpg b/devexp_demos/devtools_companion/assets/images/cauliflower.jpg new file mode 100644 index 0000000..ce2aa91 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/cauliflower.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/endive.jpg b/devexp_demos/devtools_companion/assets/images/endive.jpg new file mode 100644 index 0000000..80c5b7e Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/endive.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/fig.jpg b/devexp_demos/devtools_companion/assets/images/fig.jpg new file mode 100644 index 0000000..baaeafe Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/fig.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/grape.jpg b/devexp_demos/devtools_companion/assets/images/grape.jpg new file mode 100644 index 0000000..94cd645 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/grape.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/green_bell_pepper.jpg b/devexp_demos/devtools_companion/assets/images/green_bell_pepper.jpg new file mode 100644 index 0000000..f9fac8c Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/green_bell_pepper.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/habanero.jpg b/devexp_demos/devtools_companion/assets/images/habanero.jpg new file mode 100644 index 0000000..3d85abd Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/habanero.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/kale.jpg b/devexp_demos/devtools_companion/assets/images/kale.jpg new file mode 100644 index 0000000..7a09929 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/kale.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/kiwi.jpg b/devexp_demos/devtools_companion/assets/images/kiwi.jpg new file mode 100644 index 0000000..21be2a6 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/kiwi.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/lemon.jpg b/devexp_demos/devtools_companion/assets/images/lemon.jpg new file mode 100644 index 0000000..62bd785 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/lemon.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/lime.jpg b/devexp_demos/devtools_companion/assets/images/lime.jpg new file mode 100644 index 0000000..a9878a5 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/lime.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/mango.jpg b/devexp_demos/devtools_companion/assets/images/mango.jpg new file mode 100644 index 0000000..27a82e5 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/mango.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/mushroom.jpg b/devexp_demos/devtools_companion/assets/images/mushroom.jpg new file mode 100644 index 0000000..a12fc69 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/mushroom.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/nectarine.jpg b/devexp_demos/devtools_companion/assets/images/nectarine.jpg new file mode 100644 index 0000000..24b55b7 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/nectarine.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/orange_bell_pepper.jpg b/devexp_demos/devtools_companion/assets/images/orange_bell_pepper.jpg new file mode 100644 index 0000000..2f78910 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/orange_bell_pepper.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/persimmon.jpg b/devexp_demos/devtools_companion/assets/images/persimmon.jpg new file mode 100644 index 0000000..c0e5d07 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/persimmon.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/plum.jpg b/devexp_demos/devtools_companion/assets/images/plum.jpg new file mode 100644 index 0000000..743c7a4 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/plum.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/potato.jpg b/devexp_demos/devtools_companion/assets/images/potato.jpg new file mode 100644 index 0000000..0f09e21 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/potato.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/radicchio.jpg b/devexp_demos/devtools_companion/assets/images/radicchio.jpg new file mode 100644 index 0000000..acd8f66 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/radicchio.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/radish.jpg b/devexp_demos/devtools_companion/assets/images/radish.jpg new file mode 100644 index 0000000..0dcbfb7 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/radish.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/squash.jpg b/devexp_demos/devtools_companion/assets/images/squash.jpg new file mode 100644 index 0000000..d20b745 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/squash.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/strawberry.jpg b/devexp_demos/devtools_companion/assets/images/strawberry.jpg new file mode 100644 index 0000000..feecd8a Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/strawberry.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/tangelo.jpg b/devexp_demos/devtools_companion/assets/images/tangelo.jpg new file mode 100644 index 0000000..83c0a03 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/tangelo.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/tomato.jpg b/devexp_demos/devtools_companion/assets/images/tomato.jpg new file mode 100644 index 0000000..ab36947 Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/tomato.jpg differ diff --git a/devexp_demos/devtools_companion/assets/images/watermelon.jpg b/devexp_demos/devtools_companion/assets/images/watermelon.jpg new file mode 100644 index 0000000..43880ba Binary files /dev/null and b/devexp_demos/devtools_companion/assets/images/watermelon.jpg differ diff --git a/devexp_demos/devtools_companion/devtools_options.yaml b/devexp_demos/devtools_companion/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devexp_demos/devtools_companion/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/devexp_demos/devtools_companion/ios/.gitignore b/devexp_demos/devtools_companion/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +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 diff --git a/devexp_demos/devtools_companion/ios/Flutter/AppFrameworkInfo.plist b/devexp_demos/devtools_companion/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/devexp_demos/devtools_companion/ios/Flutter/Debug.xcconfig b/devexp_demos/devtools_companion/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/devexp_demos/devtools_companion/ios/Flutter/Release.xcconfig b/devexp_demos/devtools_companion/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/devexp_demos/devtools_companion/ios/Podfile b/devexp_demos/devtools_companion/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.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 diff --git a/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.pbxproj b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bad0d5c --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*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 */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 859F50BB9A9682AFFED2D320 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 464BA753E18C80D3117574CA /* Pods_Runner.framework */; }; + 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 */; }; + CDD605E37C66C638E52A0F7A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 85A313AD8D9D180B0CD74893 /* Pods_RunnerTests.framework */; }; +/* 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 */ + 054FB362BE923AB8B52F8282 /* 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 = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1563BC4FE7F3BFCB53A9F88F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 464BA753E18C80D3117574CA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5A1E1981940D8AB38EC2EAC3 /* 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 85A313AD8D9D180B0CD74893 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 98D87F540115B0D07691EA6C /* 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 = ""; }; + BC1EF832CA704198527FAC2D /* 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 = ""; }; + CB24B5AA7F9232A8C43035E9 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3F4980AEFBFEDC9A2960D47E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CDD605E37C66C638E52A0F7A /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 859F50BB9A9682AFFED2D320 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 388791C178C01188BCC0FBBF /* Pods */ = { + isa = PBXGroup; + children = ( + 054FB362BE923AB8B52F8282 /* Pods-Runner.debug.xcconfig */, + BC1EF832CA704198527FAC2D /* Pods-Runner.release.xcconfig */, + CB24B5AA7F9232A8C43035E9 /* Pods-Runner.profile.xcconfig */, + 1563BC4FE7F3BFCB53A9F88F /* Pods-RunnerTests.debug.xcconfig */, + 98D87F540115B0D07691EA6C /* Pods-RunnerTests.release.xcconfig */, + 5A1E1981940D8AB38EC2EAC3 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 388791C178C01188BCC0FBBF /* Pods */, + B4DEA10401457CEC0FEB0DA3 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + B4DEA10401457CEC0FEB0DA3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 464BA753E18C80D3117574CA /* Pods_Runner.framework */, + 85A313AD8D9D180B0CD74893 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 37DFBE7D49B083362F8F1257 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 3F4980AEFBFEDC9A2960D47E /* 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 = ( + 6826B31A2D969D035448735C /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + AB9F7A029FFC3E0780CF0AEE /* [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 = ( + ); + 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 */ + 37DFBE7D49B083362F8F1257 /* [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"; + }; + 6826B31A2D969D035448735C /* [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; + }; + 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"; + }; + AB9F7A029FFC3E0780CF0AEE /* [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 */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + 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 = F6TVK29MCA; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1563BC4FE7F3BFCB53A9F88F /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 98D87F540115B0D07691EA6C /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5A1E1981940D8AB38EC2EAC3 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + 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; + "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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + 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 = F6TVK29MCA; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 = F6TVK29MCA; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */; +} diff --git a/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/devexp_demos/devtools_companion/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/devexp_demos/devtools_companion/ios/Runner.xcworkspace/contents.xcworkspacedata b/devexp_demos/devtools_companion/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/devexp_demos/devtools_companion/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/devexp_demos/devtools_companion/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/devexp_demos/devtools_companion/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/devexp_demos/devtools_companion/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/devexp_demos/devtools_companion/ios/Runner/AppDelegate.swift b/devexp_demos/devtools_companion/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/devexp_demos/devtools_companion/ios/Runner/Base.lproj/LaunchScreen.storyboard b/devexp_demos/devtools_companion/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/devexp_demos/devtools_companion/ios/Runner/Base.lproj/Main.storyboard b/devexp_demos/devtools_companion/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/devexp_demos/devtools_companion/ios/Runner/Info.plist b/devexp_demos/devtools_companion/ios/Runner/Info.plist new file mode 100644 index 0000000..4a025db --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Devtools Companion + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + devtools_companion + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/devexp_demos/devtools_companion/ios/Runner/Runner-Bridging-Header.h b/devexp_demos/devtools_companion/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/devexp_demos/devtools_companion/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/devexp_demos/devtools_companion/ios/RunnerTests/RunnerTests.swift b/devexp_demos/devtools_companion/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/devexp_demos/devtools_companion/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/devexp_demos/devtools_companion/lib/driver_main.dart b/devexp_demos/devtools_companion/lib/driver_main.dart new file mode 100644 index 0000000..14122aa --- /dev/null +++ b/devexp_demos/devtools_companion/lib/driver_main.dart @@ -0,0 +1,10 @@ +import 'package:flutter_driver/driver_extension.dart'; + +import 'main.dart' as app; + +/// This entry point is used for AI agents to run the app with the flutter +/// driver extension enabled. +void main() { + enableFlutterDriverExtension(); + app.main(); +} diff --git a/devexp_demos/devtools_companion/lib/main.dart b/devexp_demos/devtools_companion/lib/main.dart new file mode 100644 index 0000000..962dd4b --- /dev/null +++ b/devexp_demos/devtools_companion/lib/main.dart @@ -0,0 +1,71 @@ +import 'dart:developer' as developer; + +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import 'src/scaffold/router.dart'; +import 'src/scaffold/theme_notifier.dart'; + +void main() { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + developer.log( + record.message, + time: record.time, + sequenceNumber: record.sequenceNumber, + level: record.level.value, + name: record.loggerName, + zone: record.zone, + error: record.error, + stackTrace: record.stackTrace, + ); + }); + + runApp( + ChangeNotifierProvider( + create: (_) => ThemeNotifier(), + child: const DevToolsCompanionApp(), + ), + ); +} + +class DevToolsCompanionApp extends StatefulWidget { + const DevToolsCompanionApp({super.key}); + + @override + State createState() => _DevToolsCompanionAppState(); +} + +class _DevToolsCompanionAppState extends State { + static const _primaryLight = Color.fromARGB(255, 25, 91, 184); + static const _primaryDark = Color.fromARGB(255, 47, 131, 249); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Consumer( + builder: (context, themeNotifier, child) => ShadApp( + title: 'DevTools Companion App', + theme: ShadThemeData( + brightness: Brightness.light, + colorScheme: const ShadSlateColorScheme.light( + primary: _primaryLight, + ), + ), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadSlateColorScheme.dark(primary: _primaryDark), + ), + themeMode: themeNotifier.themeMode, + initialRoute: home, + onGenerateRoute: onGenerateRoute, + builder: (context, child) { + return ShadToaster(child: child!); + }, + ), + ), + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/scaffold/app_drawer.dart b/devexp_demos/devtools_companion/lib/src/scaffold/app_drawer.dart new file mode 100644 index 0000000..3fdcf34 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/scaffold/app_drawer.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; + +import 'router.dart'; + +class AppDrawer extends StatelessWidget { + const AppDrawer({super.key}); + + static const _drawerWidth = 200.0; + static const _topSpacing = 60.0; + + @override + Widget build(BuildContext context) { + final currentPath = + ModalRoute.of(context)?.settings.name ?? AppRoute.home.path; + return Drawer( + width: _drawerWidth, + child: ListView( + padding: const EdgeInsets.only(top: _topSpacing), + children: AppRoute.values + .where((route) => route.showInAppDrawer) + .map((route) => _DrawerItem(route: route, currentPath: currentPath)) + .toList(), + ), + ); + } +} + +class _DrawerItem extends StatelessWidget { + const _DrawerItem({required this.route, required this.currentPath}); + + final AppRoute route; + final String currentPath; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isSelected = currentPath == route.path; + final color = isSelected + ? theme.colorScheme.primary + : theme.textTheme.bodyLarge?.color; + + return ListTile( + leading: AssetImageIcon(asset: route.iconAsset, color: color), + title: Text( + route.name, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: color, + ), + ), + selected: isSelected, + onTap: () async { + Navigator.pop(context); + if (!isSelected) { + await Navigator.pushReplacementNamed(context, route.path); + } + }, + ); + } +} + +/// A widget that renders an [asset] image styled as an icon. +final class AssetImageIcon extends StatelessWidget { + const AssetImageIcon({super.key, required this.asset, this.color}); + + static const _iconSize = 18.0; + + final String asset; + final Color? color; + + @override + Widget build(BuildContext context) { + return Image( + image: AssetImage(asset), + height: _iconSize, + width: _iconSize, + fit: BoxFit.fill, + color: color ?? Theme.of(context).colorScheme.onSurface, + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/scaffold/app_shell.dart b/devexp_demos/devtools_companion/lib/src/scaffold/app_shell.dart new file mode 100644 index 0000000..79d061a --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/scaffold/app_shell.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import './app_drawer.dart'; +import './theme_notifier.dart'; + +class AppShell extends StatelessWidget { + const AppShell({ + super.key, + required this.screenName, + required this.screenBody, + this.showDrawer = true, + }); + + final String screenName; + final Widget screenBody; + final bool showDrawer; + + @override + Widget build(BuildContext context) { + final themeNotifier = Provider.of(context); + final isDarkMode = themeNotifier.themeMode == ThemeMode.dark; + + return Scaffold( + appBar: AppBar( + title: Text(screenName), + actions: [ + IconButton( + icon: Icon(isDarkMode ? Icons.light_mode : Icons.dark_mode), + onPressed: () { + themeNotifier.toggleTheme(); + }, + tooltip: isDarkMode ? 'Light mode' : 'Dark mode', + ), + ], + ), + drawer: showDrawer ? const AppDrawer() : null, + body: Center(child: screenBody), + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/scaffold/router.dart b/devexp_demos/devtools_companion/lib/src/scaffold/router.dart new file mode 100644 index 0000000..bf20ea5 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/scaffold/router.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; + +import '../screens/cpu_profiler/cpu_profiler_screen.dart'; +import '../screens/debugger/debugger_screen.dart'; +import '../screens/home/home_screen.dart'; +import '../screens/inspector/inspector_screen.dart'; +import '../screens/logging/logging_screen.dart'; +import '../screens/memory/memory_screen.dart'; +import '../screens/network/network_screen.dart'; +import '../screens/performance/data/fruits.dart'; +import '../screens/performance/fruit_details.dart'; +import '../screens/performance/performance_screen.dart'; +import '../screens/timeline/timeline_screen.dart'; +import './app_shell.dart'; + +enum AppRoute { + home(name: 'Home', path: '/', iconAsset: 'assets/icons/devtools.png'), + inspector( + name: 'Inspector', + path: '/inspector', + iconAsset: 'assets/icons/inspector.png', + ), + performance( + name: 'Performance', + path: '/performance', + iconAsset: 'assets/icons/performance.png', + ), + performanceDetails( + name: 'Performance Details', + path: '/performance/details', + iconAsset: 'assets/icons/performance.png', + showInAppDrawer: false, + ), + timeline( + name: 'Timeline', + path: '/timeline', + // TODO: Choose a different icon. + iconAsset: 'assets/icons/debugger.png', + ), + network( + name: 'Network', + path: '/network', + iconAsset: 'assets/icons/network.png', + ), + memory(name: 'Memory', path: '/memory', iconAsset: 'assets/icons/memory.png'), + cpuProfiler( + name: 'CPU Profiler', + path: '/cpu-profiler', + iconAsset: 'assets/icons/cpu_profiler.png', + ), + debugger( + name: 'Debugger', + path: '/debugger', + iconAsset: 'assets/icons/debugger.png', + ), + logging( + name: 'Logging', + path: '/logging', + iconAsset: 'assets/icons/logging.png', + ); + + const AppRoute({ + required this.name, + required this.path, + required this.iconAsset, + this.showInAppDrawer = true, + }); + final String path; + final String name; + final String iconAsset; + final bool showInAppDrawer; + + static AppRoute fromPath(String? path) { + if (path == null) return AppRoute.home; + return AppRoute.values.firstWhere( + (e) => e.path == path, + orElse: () => AppRoute.home, + ); + } +} + +String get home => AppRoute.home.path; + +Route onGenerateRoute(RouteSettings settings) { + final page = AppRoute.fromPath(settings.name); + return MaterialPageRoute( + settings: settings, + builder: (context) { + switch (page) { + case AppRoute.inspector: + return const AppShell( + screenName: 'Inspector', + screenBody: InspectorScreen(), + ); + case AppRoute.performance: + return const AppShell( + screenName: 'Performance', + screenBody: PerformanceScreen(), + ); + case AppRoute.performanceDetails: + final fruit = settings.arguments as Fruit; + return AppShell( + screenName: 'Performance', + screenBody: FruitDetailsScreen(fruit: fruit), + showDrawer: false, + ); + case AppRoute.timeline: + return const AppShell( + screenName: 'Timeline', + screenBody: TimelineScreen(), + ); + case AppRoute.network: + return const AppShell( + screenName: 'Network', + screenBody: NetworkScreen(), + ); + case AppRoute.memory: + return const AppShell( + screenName: 'Memory', + screenBody: MemoryScreen(), + ); + case AppRoute.cpuProfiler: + return const AppShell( + screenName: 'CPU Profiler', + screenBody: CpuProfilerScreen(), + ); + case AppRoute.debugger: + return const AppShell( + screenName: 'Debugger', + screenBody: DebuggerScreen(), + ); + case AppRoute.logging: + return const AppShell( + screenName: 'Logging', + screenBody: LoggingScreen(), + ); + case AppRoute.home: + return const AppShell(screenName: 'Home', screenBody: HomeScreen()); + } + }, + ); +} diff --git a/devexp_demos/devtools_companion/lib/src/scaffold/theme_notifier.dart b/devexp_demos/devtools_companion/lib/src/scaffold/theme_notifier.dart new file mode 100644 index 0000000..9e19ef3 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/scaffold/theme_notifier.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class ThemeNotifier extends ChangeNotifier { + ThemeMode _themeMode = ThemeMode.light; + + ThemeMode get themeMode => _themeMode; + + void toggleTheme() { + _themeMode = + _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; + notifyListeners(); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/cpu_profiler/cpu_profiler_screen.dart b/devexp_demos/devtools_companion/lib/src/screens/cpu_profiler/cpu_profiler_screen.dart new file mode 100644 index 0000000..bca150c --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/cpu_profiler/cpu_profiler_screen.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'dart:isolate'; + +import 'package:flutter/material.dart'; + +import '../../shared/ui/theme.dart'; +import '../../shared/widgets/expensive_task_widget.dart'; +import 'party_poppers.dart'; + +class CpuProfilerScreen extends StatefulWidget { + const CpuProfilerScreen({super.key}); + + @override + State createState() => _CpuProfilerScreenState(); +} + +class _CpuProfilerScreenState extends State { + final _partyPopperController = PartyPopperController(); + + @override + Widget build(BuildContext context) { + return ListView( + padding: const .all(largePadding), + children: [ + const ExpensiveTaskWidget( + title: 'Compute Fibonacci (37)', + task: runFib37, + ), + ExpensiveTaskWidget( + title: 'Compute Fibonacci (37) in Isolate', + task: () => Isolate.run(runFib37), + ), + ExpensiveTaskWidget( + title: 'Compute FibonacciAsync (20) x5', + task: () async { + final results = await Future.wait([ + for (var i = 0; i < 5; i++) fibAsync20(), + ]); + return '${results.reduce((a, b) => a + b)}'; + }, + ), + ExpensiveTaskWidget( + title: 'Create 100 Party Poppers', + children: [PartyPopperManager(controller: _partyPopperController)], + task: () async { + await _partyPopperController.start(100); + return 'Hooray!'; + }, + ), + ], + ); + } +} + +Future fibAsync20() => _fibAsync(20); + +Future _fibAsync(int n) async { + if (n <= 1) return n; + await Future.delayed(const Duration(milliseconds: 16)); + final (a, b) = await (_fibAsync(n - 1), _fibAsync(n - 2)).wait; + return a + b; +} + +String runFib37() => _fib(37).toString(); + +int _fib(int n) { + if (n <= 1) return n; + return _fib(n - 1) + _fib(n - 2); +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/cpu_profiler/party_poppers.dart b/devexp_demos/devtools_companion/lib/src/screens/cpu_profiler/party_poppers.dart new file mode 100644 index 0000000..4f97d6b --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/cpu_profiler/party_poppers.dart @@ -0,0 +1,210 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +class PartyPopperController { + _PartyPopperManagerState? _state; + + void _attach(_PartyPopperManagerState state) { + _state = state; + } + + void _detach() { + _state = null; + } + + Future start(int count) async { + if (_state != null) { + await _state!.addPoppers(count); + } + } +} + +class PartyPopperManager extends StatefulWidget { + const PartyPopperManager({super.key, required this.controller}); + + final PartyPopperController controller; + + @override + State createState() => _PartyPopperManagerState(); +} + +class _PartyPopperManagerState extends State { + int _count = 0; + int _runId = 0; + List> _completers = []; + + @override + void initState() { + super.initState(); + widget.controller._attach(this); + } + + @override + void didUpdateWidget(PartyPopperManager oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller._detach(); + widget.controller._attach(this); + } + } + + @override + void dispose() { + widget.controller._detach(); + for (final c in _completers) { + if (!c.isCompleted) c.complete(); + } + _completers = []; + super.dispose(); + } + + Future addPoppers(int count) async { + if (!mounted) return; + if (_completers.isNotEmpty) return; + + // Complete any previous run to avoid hanging + for (final c in _completers) { + if (!c.isCompleted) c.complete(); + } + + setState(() { + _count = count; + _runId++; + _completers = List.generate(count, (_) => Completer()); + }); + + // Wait for all animations to complete + await Future.wait(_completers.map((c) => c.future)); + if (!mounted) return; + setState(() { + _completers = []; + }); + } + + @override + Widget build(BuildContext context) { + if (_completers.isEmpty) return const SizedBox.shrink(); + return Expanded( + child: SizedBox( + height: 300, + child: LayoutBuilder( + builder: (context, constraints) { + return Stack( + children: List.generate(_count, (index) { + final random = Random(index); + return Positioned( + left: random.nextDouble() * constraints.maxWidth, + top: random.nextDouble() * constraints.maxHeight, + child: PartyPopper( + key: ValueKey('$_runId-$index'), + seed: index, + delay: Duration(milliseconds: index * 50), + onDone: () { + if (index < _completers.length && + !_completers[index].isCompleted) { + _completers[index].complete(); + } + }, + ), + ); + }), + ); + }, + ), + ), + ); + } +} + +class PartyPopper extends StatefulWidget { + const PartyPopper({ + super.key, + this.seed = 0, + this.delay = Duration.zero, + this.onDone, + }); + + final int seed; + final Duration delay; + final VoidCallback? onDone; + + @override + State createState() => _PartyPopperState(); +} + +class _PartyPopperState extends State { + bool _visible = false; + + @override + void initState() { + super.initState(); + if (widget.delay == Duration.zero) { + _visible = true; + } else { + Future.delayed(widget.delay, () { + if (mounted) { + setState(() { + _visible = true; + }); + } + }); + } + } + + @override + Widget build(BuildContext context) { + if (!_visible) return const SizedBox.shrink(); + + return IgnorePointer( + child: Stack( + alignment: Alignment.center, + children: [ + // Confetti + ...List.generate(50, (index) { + final random = Random(widget.seed * 1000 + index); + final angle = pi + random.nextDouble() * pi; + final distance = 100 + random.nextDouble() * 200; + final color = + Colors.primaries[random.nextInt(Colors.primaries.length)]; + + return Positioned( + child: + Container( + width: 2, + height: 2, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ) + .animate() + .move( + delay: 0.ms, + duration: 1500.ms, + begin: Offset.zero, + end: Offset( + cos(angle) * distance, + sin(angle) * distance, + ), + curve: Curves.easeOut, + ) + .fadeOut(delay: 500.ms, duration: 500.ms), + ); + }), + const Text('🎉', style: TextStyle(fontSize: 16)) + .animate(onComplete: (controller) => widget.onDone?.call()) + .scale(duration: 500.ms, curve: Curves.elasticOut) + .shake(delay: 500.ms, duration: 500.ms) + .fadeOut( + delay: 1000.ms, + duration: 1000.ms, + curve: Curves.elasticOut, + ), + ], + ), + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/debugger/debugger_screen.dart b/devexp_demos/devtools_companion/lib/src/screens/debugger/debugger_screen.dart new file mode 100644 index 0000000..69c487d --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/debugger/debugger_screen.dart @@ -0,0 +1,195 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../shared/ui/theme.dart'; + +class DebuggerScreen extends StatefulWidget { + const DebuggerScreen({super.key}); + + @override + State createState() => _DebuggerScreenState(); +} + +class _DebuggerScreenState extends State { + // --- Inspectable Data Structures --- + final List _people = []; + final Map _complexMap = { + 'config': { + 'version': 1.0, + 'features': ['fast', 'secure', 'modern'], + }, + 'metadata': {'cratedAt': DateTime.now().toIso8601String()}, + }; + final Set _uniqueIds = {}; + final LinkedList _linkedList = LinkedList(); + + Node? _binaryTreeRoot; + + Timer? _tickerTimer; + int _tickerSeconds = 0; + + int _counter = 0; + + @override + void initState() { + super.initState(); + _initializeData(); + _startTicker(); + } + + @override + void dispose() { + _tickerTimer?.cancel(); + super.dispose(); + } + + void _startTicker() { + _tickerTimer = Timer.periodic(const Duration(seconds: 1), _onTick); + } + + void _onTick(Timer timer) { + setState(() { + _tickerSeconds++; + }); + // PLACE BREAKPOINT HERE to evaluate the code in the DevTools debugger + // + debugPrint('Ticker tick: $_tickerSeconds'); + } + + void _initializeData() { + // Populate heavy data for inspection + for (int i = 0; i < 5; i++) { + _people.add( + Person( + name: 'User $i', + age: 20 + i, + attributes: {'role': i % 2 == 0 ? 'Admin' : 'User'}, + ), + ); + } + + _uniqueIds.addAll(['ID-A', 'ID-B', 'ID-C']); + + _linkedList.addAll([CustomEntry(100), CustomEntry(200), CustomEntry(300)]); + + _binaryTreeRoot = Node( + id: 1, + left: Node(id: 2, left: Node(id: 4), right: Node(id: 5)), + right: Node(id: 3, right: Node(id: 6)), + ); + } + + void _addPerson() { + setState(() { + _people.add(Person(name: 'New User $_counter', age: 10 + _counter)); + _counter++; + }); + } + + void _modifyMap() { + setState(() { + _complexMap['update_$_counter'] = Random().nextInt(1000); + (_complexMap['config'] as Map)['last_updated'] = DateTime.now(); + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(largeSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Debugger Object Zoo', + style: ShadTheme.of(context).textTheme.h3, + ), + Text( + 'Use Flutter DevTools "Debugger" to inspect these objects.', + style: ShadTheme.of(context).textTheme.p, + ), + const SizedBox(height: largeSpacing), + ShadCard( + title: const Text('Breakpoint Target'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ticker Running: $_tickerSeconds s', + style: const TextStyle( + fontFamily: 'RobotoMono', + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: denseSpacing), + const Text( + 'Instructions: Open DevTools Debugger and place a breakpoint inside the "_onTick" method in debugger_screen.dart to pause execution repeatedly.', + ), + ], + ), + ), + const SizedBox(height: largeSpacing), + ShadCard( + title: const Text('Data Mutations'), + child: Wrap( + spacing: denseSpacing, + runSpacing: denseSpacing, + children: [ + ShadButton.outline( + onPressed: _addPerson, + child: const Text('Add Person (List)'), + ), + ShadButton.outline( + onPressed: _modifyMap, + child: const Text('Modify Complex Map'), + ), + ShadButton.outline( + onPressed: () => setState(() { + _uniqueIds.add('ID-$_counter'); + _counter++; + }), + child: const Text('AddTo Set'), + ), + ], + ), + ), + ], + ), + ); + } +} + +// --- Custom Objects --- + +class Person { + Person({required this.name, required this.age, this.attributes}); + + final String name; + final int age; + final Map? attributes; + final List friends = []; + + @override + String toString() => '$name ($age)'; +} + +class Node { + Node({required this.id, this.left, this.right}); + + final int id; + Node? left; + Node? right; +} + +base class CustomEntry extends LinkedListEntry { + CustomEntry(this.value); + final int value; + + @override + String toString() => 'Entry($value)'; +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/home/home_screen.dart b/devexp_demos/devtools_companion/lib/src/screens/home/home_screen.dart new file mode 100644 index 0000000..f332755 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/home/home_screen.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../shared/ui/theme.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + String? _readmeContent; + + @override + void initState() { + super.initState(); + unawaited(_loadReadme()); + } + + Future _loadReadme() async { + final content = await rootBundle.loadString('README.md'); + setState(() { + _readmeContent = content; + }); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(denseSpacing), + child: ShadCard( + child: _readmeContent == null + ? const Center(child: ShadProgress()) + : Markdown(data: _readmeContent!, shrinkWrap: true), + ), + ), + ], + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/inspector/inspector_screen.dart b/devexp_demos/devtools_companion/lib/src/screens/inspector/inspector_screen.dart new file mode 100644 index 0000000..ef9365b --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/inspector/inspector_screen.dart @@ -0,0 +1,479 @@ +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import '../../shared/ui/theme.dart'; + +class Meal { + const Meal({ + required this.name, + required this.description, + required this.ingredients, + }); + + final String name; + final String description; + final List ingredients; +} + +const meals = [ + Meal( + name: 'Overflowing Oatmeal', + description: 'A healthy and hearty breakfast.', + ingredients: [ + 'Rolled oats', + 'Milk', + 'Honey', + 'Berries', + 'A very long line of text that will definitely overflow the screen and cause a render overflow error.', + ], + ), + Meal( + name: 'Salad', + description: 'A light and refreshing lunch.', + ingredients: ['Lettuce', 'Tomato', 'Cucumber', 'Dressing'], + ), + Meal( + name: 'Spaghetti Carbonara', + description: 'A classic Italian pasta dish.', + ingredients: [ + 'Spaghetti', + 'Eggs', + 'Pancetta', + 'Parmesan cheese', + 'Black pepper', + ], + ), +]; + +enum CalendarView { month, week } + +class InspectorScreen extends StatefulWidget { + const InspectorScreen({super.key}); + + @override + State createState() => _InspectorScreenState(); +} + +class _InspectorScreenState extends State { + DateTime? _selectedDate; + Meal? _selectedMeal; + CalendarView _calendarView = CalendarView.month; + + void _onDateSelected(DateTime date) { + setState(() { + _selectedDate = date; + }); + } + + void _showCalendar() { + setState(() { + _selectedDate = null; + }); + } + + void _onMealSelected(Meal? meal) { + setState(() { + _selectedMeal = meal; + }); + } + + void _onCalendarViewChanged(CalendarView view) { + setState(() { + _calendarView = view; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Meal Planner')), + body: Column( + children: [ + Expanded( + child: _selectedDate == null + ? CalendarWidget( + onDateSelected: _onDateSelected, + initialView: _calendarView, + onViewChanged: _onCalendarViewChanged, + ) + : DailyMealPlan( + date: _selectedDate!, + onTap: () { + _showCalendar(); + _onMealSelected(null); + }, + onMealSelected: _onMealSelected, + ), + ), + Expanded(child: RecipeWidget(meal: _selectedMeal)), + ], + ), + ); + } +} + +class DailyMealPlan extends StatelessWidget { + const DailyMealPlan({ + super.key, + required this.date, + required this.onTap, + required this.onMealSelected, + }); + + final DateTime date; + final VoidCallback onTap; + final ValueChanged onMealSelected; + + @override + Widget build(BuildContext context) { + final dayName = DateFormat('EEEE').format(date); + return Stack( + children: [ + Positioned( + top: noPadding, + left: noPadding, + child: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: onTap, + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + '${date.month}/${date.day}/${date.year} - $dayName', + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + const SizedBox(height: largeSpacing), + Table( + columnWidths: const { + 0: IntrinsicColumnWidth(), + 1: IntrinsicColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + const Padding( + padding: EdgeInsets.only(right: denseSpacing), + child: Text('Breakfast:'), + ), + Align( + alignment: Alignment.centerLeft, + child: ShadButton.ghost( + onPressed: () => onMealSelected(meals[0]), + child: Text(meals[0].name), + ), + ), + ], + ), + TableRow( + children: [ + const Padding( + padding: EdgeInsets.only(right: denseSpacing), + child: Text('Lunch:'), + ), + Align( + alignment: Alignment.centerLeft, + child: ShadButton.ghost( + onPressed: () => onMealSelected(meals[1]), + child: Text(meals[1].name), + ), + ), + ], + ), + TableRow( + children: [ + const Padding( + padding: EdgeInsets.only(right: denseSpacing), + child: Text('Dinner:'), + ), + Align( + alignment: Alignment.centerLeft, + child: ShadButton.ghost( + onPressed: () => onMealSelected(meals[2]), + child: Text(meals[2].name), + ), + ), + ], + ), + ], + ), + ], + ), + ], + ); + } +} + +class CalendarWidget extends StatefulWidget { + const CalendarWidget({ + super.key, + required this.onDateSelected, + required this.initialView, + required this.onViewChanged, + }); + + final ValueChanged onDateSelected; + final CalendarView initialView; + final ValueChanged onViewChanged; + + @override + State createState() => _CalendarWidgetState(); +} + +class _CalendarWidgetState extends State { + late CalendarView _view; + + @override + void initState() { + super.initState(); + _view = widget.initialView; + } + + @override + Widget build(BuildContext context) { + return ShadTabs( + value: _view, + tabs: [ + ShadTab( + value: CalendarView.month, + content: MonthViewWidget(onDateSelected: widget.onDateSelected), + child: const Text('Month'), + ), + ShadTab( + value: CalendarView.week, + content: WeekViewWidget(onDateSelected: widget.onDateSelected), + child: const Text('Week'), + ), + ], + ); + } +} + +class MonthViewWidget extends StatefulWidget { + const MonthViewWidget({super.key, required this.onDateSelected}); + + final ValueChanged onDateSelected; + + @override + State createState() => _MonthViewWidgetState(); +} + +class _MonthViewWidgetState extends State { + late DateTime _currentMonth; + + @override + void initState() { + super.initState(); + final now = DateTime.now(); + _currentMonth = DateTime(now.year, now.month); + } + + void _previousMonth() { + setState(() { + _currentMonth = DateTime(_currentMonth.year, _currentMonth.month - 1); + }); + } + + void _nextMonth() { + setState(() { + _currentMonth = DateTime(_currentMonth.year, _currentMonth.month + 1); + }); + } + + @override + Widget build(BuildContext context) { + final monthName = DateFormat('MMMM yyyy').format(_currentMonth); + final daysInMonth = DateTime( + _currentMonth.year, + _currentMonth.month + 1, + 0, + ).day; + final firstDayOfMonth = DateTime(_currentMonth.year, _currentMonth.month); + final weekdayOfFirstDay = + firstDayOfMonth.weekday; // Monday is 1, Sunday is 7 + + final List calendarItems = []; + for (int i = 1; i < weekdayOfFirstDay; i++) { + calendarItems.add(Container()); + } + + for (int i = 1; i <= daysInMonth; i++) { + final date = DateTime(_currentMonth.year, _currentMonth.month, i); + calendarItems.add( + InkWell( + onTap: () => widget.onDateSelected(date), + child: Center(child: Text('$i')), + ), + ); + } + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: _previousMonth, + ), + Text(monthName), + IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: _nextMonth, + ), + ], + ), + const Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text('Mon'), + Text('Tue'), + Text('Wed'), + Text('Thu'), + Text('Fri'), + Text('Sat'), + Text('Sun'), + ], + ), + GridView.count( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + crossAxisCount: 7, + childAspectRatio: 1.5, + children: calendarItems, + ), + ], + ); + } +} + +class WeekViewWidget extends StatefulWidget { + const WeekViewWidget({super.key, required this.onDateSelected}); + + final ValueChanged onDateSelected; + + @override + State createState() => _WeekViewWidgetState(); +} + +class _WeekViewWidgetState extends State { + late DateTime _currentWeekStart; + + @override + void initState() { + super.initState(); + final now = DateTime.now(); + // Start the week on the Monday of the current week. + _currentWeekStart = now.subtract(Duration(days: now.weekday - 1)); + } + + void _previousWeek() { + setState(() { + _currentWeekStart = _currentWeekStart.subtract(const Duration(days: 7)); + }); + } + + void _nextWeek() { + setState(() { + _currentWeekStart = _currentWeekStart.add(const Duration(days: 7)); + }); + } + + @override + Widget build(BuildContext context) { + final weekEnd = _currentWeekStart.add(const Duration(days: 6)); + final formatter = DateFormat('MMM d'); + final headerText = + '${formatter.format(_currentWeekStart)} - ${formatter.format(weekEnd)}'; + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: _previousWeek, + ), + Text(headerText), + IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: _nextWeek, + ), + ], + ), + const SizedBox(height: denseSpacing), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(7, (dayIndex) { + final date = _currentWeekStart.add(Duration(days: dayIndex)); + return InkWell( + onTap: () => widget.onDateSelected(date), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(DateFormat('E').format(date)), // Day of week (e.g., Mon) + Text('${date.day}'), + ], + ), + ); + }), + ), + ], + ); + } +} + +class RecipeWidget extends StatelessWidget { + const RecipeWidget({super.key, required this.meal}); + + final Meal? meal; + + @override + Widget build(BuildContext context) { + return ShadCard( + child: Padding( + padding: const EdgeInsets.all(largeSpacing), + child: meal == null + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Select a meal to view its recipe.', + style: ShadTheme.of(context).textTheme.h4, + ), + const SizedBox(height: largeSpacing), + Image.asset('assets/images/habanero.jpg', height: 200), + ], + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + meal!.name, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: denseSpacing), + Text(meal!.description), + const SizedBox(height: largeSpacing), + const Text( + 'Ingredients:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: denseSpacing), + for (final ingredient in meal!.ingredients) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [const Text('- '), Text(ingredient)], + ), + ], + ), + ), + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/logging/logging_screen.dart b/devexp_demos/devtools_companion/lib/src/screens/logging/logging_screen.dart new file mode 100644 index 0000000..0df2cd6 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/logging/logging_screen.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../shared/ui/theme.dart'; + +final _log = Logger('devtools_companion'); + +class LoggingScreen extends StatefulWidget { + const LoggingScreen({super.key}); + + @override + State createState() => _LoggingScreenState(); +} + +class _LoggingScreenState extends State { + static const String _defaultText = 'Hello world!'; + + late TextEditingController _finestController; + late TextEditingController _finerController; + late TextEditingController _fineController; + late TextEditingController _configController; + late TextEditingController _logController; + late TextEditingController _warnController; + late TextEditingController _severeController; + late TextEditingController _shoutController; + + @override + void initState() { + super.initState(); + _finestController = TextEditingController(text: _defaultText); + _finerController = TextEditingController(text: _defaultText); + _fineController = TextEditingController(text: _defaultText); + _configController = TextEditingController(text: _defaultText); + _logController = TextEditingController(text: _defaultText); + _warnController = TextEditingController(text: _defaultText); + _severeController = TextEditingController(text: _defaultText); + _shoutController = TextEditingController(text: _defaultText); + } + + @override + void dispose() { + _finestController.dispose(); + _finerController.dispose(); + _fineController.dispose(); + _configController.dispose(); + _logController.dispose(); + _warnController.dispose(); + _severeController.dispose(); + _shoutController.dispose(); + super.dispose(); + } + + void _triggerFinestLog() { + final message = _finestController.text; + _log.finest(message); + _showSnackBar('Finest log sent: $message'); + } + + void _triggerFinerLog() { + final message = _finerController.text; + _log.finer(message); + _showSnackBar('Finer log sent: $message'); + } + + void _triggerFineLog() { + final message = _fineController.text; + _log.fine(message); + _showSnackBar('Fine log sent: $message'); + } + + void _triggerConfigLog() { + final message = _configController.text; + _log.config(message); + _showSnackBar('Config log sent: $message'); + } + + void _triggerInfoLog() { + final message = _logController.text; + _log.info(message); + _showSnackBar('Info log sent: $message'); + } + + void _triggerWarningLog() { + final message = _warnController.text; + _log.warning(message); + _showSnackBar('Warning log sent: $message'); + } + + void _triggerSevereLog() { + final message = _severeController.text; + _log.severe(message, Exception(message), StackTrace.current); + _showSnackBar('Severe log sent: $message'); + } + + void _triggerShoutLog() { + final message = _shoutController.text; + _log.shout(message); + _showSnackBar('Shout log sent: $message'); + } + + void _showSnackBar(String text) { + final toast = ShadToast( + description: Text(text), + duration: const Duration(milliseconds: 1200), + ); + ShadToaster.of(context).show(toast); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return Padding( + padding: const EdgeInsets.all(largeSpacing), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Trigger Developer Logs', style: theme.textTheme.h2), + const SizedBox(height: extraLargeSpacing), + _LogInputRow( + label: 'log (Finest)', + controller: _finestController, + buttonColor: Colors.lightGreen, + icon: Icons.info_outline, + onPressed: _triggerFinestLog, + ), + const SizedBox(height: largeSpacing), + _LogInputRow( + label: 'log (Finer)', + controller: _finerController, + buttonColor: Colors.green.shade400, + icon: Icons.info_outline, + onPressed: _triggerFinerLog, + ), + const SizedBox(height: largeSpacing), + _LogInputRow( + label: 'log (Fine)', + controller: _fineController, + buttonColor: Colors.green, + icon: Icons.info_outline, + onPressed: _triggerFineLog, + ), + const SizedBox(height: largeSpacing), + _LogInputRow( + label: 'log (Config)', + controller: _configController, + buttonColor: Colors.teal, + icon: Icons.settings, + onPressed: _triggerConfigLog, + ), + const SizedBox(height: largeSpacing), + _LogInputRow( + label: 'log (Info)', + controller: _logController, + buttonColor: Colors.blue, + icon: Icons.info_outline, + onPressed: _triggerInfoLog, + ), + const SizedBox(height: largeSpacing), + _LogInputRow( + label: 'log (Warning)', + controller: _warnController, + buttonColor: Colors.orange, + icon: Icons.warning_amber_rounded, + onPressed: _triggerWarningLog, + ), + const SizedBox(height: largeSpacing), + _LogInputRow( + label: 'log (Severe)', + controller: _severeController, + buttonColor: Colors.red, + icon: Icons.warning_amber_rounded, + onPressed: _triggerSevereLog, + ), + const SizedBox(height: largeSpacing), + _LogInputRow( + label: 'log (Shout)', + controller: _shoutController, + buttonColor: Colors.purple, + icon: Icons.campaign, + onPressed: _triggerShoutLog, + ), + ], + ), + ), + ); + } +} + +class _LogInputRow extends StatelessWidget { + const _LogInputRow({ + required this.label, + required this.controller, + required this.buttonColor, + required this.icon, + required this.onPressed, + }); + + final String label; + final TextEditingController controller; + final Color buttonColor; + final IconData icon; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: TextField( + controller: controller, + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon, color: buttonColor), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: buttonColor, width: 2), + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + const SizedBox(width: defaultSpacing), + SizedBox( + height: 56, + child: ShadButton(onPressed: onPressed, child: const Text('Log')), + ), + ], + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/memory/memory_screen.dart b/devexp_demos/devtools_companion/lib/src/screens/memory/memory_screen.dart new file mode 100644 index 0000000..b257ad2 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/memory/memory_screen.dart @@ -0,0 +1,281 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import '../../shared/ui/theme.dart'; + +const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + +void _generateObjectsIsolate(Map args) async { + final sendPort = args['sendPort'] as SendPort; + final objectCount = args['objectCount'] as int; + final minSize = args['minSize'] as int; + final maxSize = args['maxSize'] as int; + final random = Random(); + final allocatedObjects = []; + final batch = []; + final stopwatch = Stopwatch()..start(); + + for (var i = 0; i < objectCount; i++) { + final length = random.nextInt(maxSize - minSize + 1) + minSize; + final randomString = String.fromCharCodes( + Iterable.generate( + length, + (_) => _chars.codeUnitAt(random.nextInt(_chars.length)), + ), + ); + + allocatedObjects.add(randomString); + batch.add(randomString); + if (stopwatch.elapsedMilliseconds > 50 || batch.length >= 500) { + sendPort.send({ + 'type': 'progress', + 'count': i + 1, + 'batch': List.from(batch), + }); + batch.clear(); + await Future.delayed(Duration.zero); + stopwatch.reset(); + } + } + if (batch.isNotEmpty) { + sendPort.send({ + 'type': 'progress', + 'count': allocatedObjects.length, + 'batch': List.from(batch), + }); + } + sendPort.send({ + 'type': 'complete', + 'count': allocatedObjects.length, + 'batch': [], + }); +} + +class MemoryScreen extends StatefulWidget { + const MemoryScreen({super.key}); + + @override + State createState() => _MemoryScreenState(); +} + +class _MemoryScreenState extends State { + late final TextEditingController _objectCountController; + late final TextEditingController _minSizeController; + late final TextEditingController _maxSizeController; + final _allocatedObjects = []; + bool _isGenerating = false; + int _generationProgress = 0; + Isolate? _isolate; + ReceivePort? _receivePort; + + @override + void initState() { + super.initState(); + _objectCountController = TextEditingController(text: '100000'); + _minSizeController = TextEditingController(text: '1000'); + _maxSizeController = TextEditingController(text: '100000'); + } + + @override + void dispose() { + _objectCountController.dispose(); + _minSizeController.dispose(); + _maxSizeController.dispose(); + _isolate?.kill(priority: Isolate.immediate); + _receivePort?.close(); + super.dispose(); + } + + Future _allocateObjects() async { + setState(() { + _isGenerating = true; + _generationProgress = 0; + }); + + final objectCount = int.tryParse(_objectCountController.text) ?? 100000; + final minSize = int.tryParse(_minSizeController.text) ?? 1000; + final maxSize = int.tryParse(_maxSizeController.text) ?? 100000; + + if (minSize > maxSize) { + ShadToaster.of(context).show( + const ShadToast( + title: Text('Error'), + description: Text('Min size cannot be greater than max size.'), + ), + ); + setState(() { + _isGenerating = false; + }); + return; + } + + _receivePort = ReceivePort(); + _isolate = await Isolate.spawn(_generateObjectsIsolate, { + 'sendPort': _receivePort!.sendPort, + 'objectCount': objectCount, + 'minSize': minSize, + 'maxSize': maxSize, + }); + + _receivePort!.listen((message) { + if (message is Map) { + switch (message['type']) { + case 'progress': + final newObjects = + (message['batch'] as List? ?? []).cast(); + if (newObjects.isNotEmpty) { + _allocatedObjects.addAll(newObjects); + } + setState(() { + _generationProgress = message['count'] as int; + }); + break; + case 'complete': + final newObjects = + (message['batch'] as List? ?? []).cast(); + if (newObjects.isNotEmpty) { + _allocatedObjects.addAll(newObjects); + } + ShadToaster.of(context).show( + ShadToast( + description: Text( + 'Allocated ${_allocatedObjects.length} string objects.', + ), + ), + ); + _stopGeneration(); + break; + } + } + }); + } + + void _stopGeneration() { + _isolate?.kill(priority: Isolate.immediate); + _receivePort?.close(); + setState(() { + _isolate = null; + _receivePort = null; + _isGenerating = false; + _generationProgress = 0; + }); + } + + void _clearObjects() { + _allocatedObjects.clear(); + setState(() {}); + ShadToaster.of(context).show( + const ShadToast( + description: Text('Cleared allocated objects. Ready for GC.'), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ListView( + padding: const EdgeInsets.all(defaultSpacing), + children: [ + Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: ShadCard( + title: const Text('Memory Tests'), + description: Text( + _isGenerating + ? 'Generating objects... $_generationProgress so far.' + : 'Use these tools to test memory management in your application.\n' + 'Currently holding onto ${_allocatedObjects.length} string objects.', + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (_isGenerating) ...[ + const ShadProgress(), + const Padding( + padding: EdgeInsets.all(defaultSpacing), + ), + const Padding( + padding: EdgeInsets.all(defaultSpacing), + ), + Semantics( + label: 'stop generation', + button: true, + child: ShadButton.destructive( + onPressed: _stopGeneration, + child: const Text('Stop Generation'), + ), + ), + ] else ...[ + Semantics( + label: 'allocate objects', + button: true, + child: ShadButton( + onPressed: _allocateObjects, + child: const Text('Allocate Objects'), + ), + ), + ], + const Padding( + padding: EdgeInsets.all(defaultSpacing), + ), + Semantics( + label: 'number of objects', + child: ShadInputFormField( + key: const ValueKey('numberOfObjectsField'), + controller: _objectCountController, + label: const Text('Number of Objects'), + keyboardType: TextInputType.number, + ), + ), + const Padding( + padding: EdgeInsets.all(defaultSpacing), + ), + Semantics( + label: 'minimum string size', + child: ShadInputFormField( + key: const ValueKey('minStringSizeField'), + controller: _minSizeController, + label: const Text('Min String Size'), + keyboardType: TextInputType.number, + ), + ), + const Padding( + padding: EdgeInsets.all(defaultSpacing), + ), + Semantics( + label: 'maximum string size', + child: ShadInputFormField( + key: const ValueKey('maxStringSizeField'), + controller: _maxSizeController, + label: const Text('Max String Size'), + keyboardType: TextInputType.number, + ), + ), + const Padding( + padding: EdgeInsets.all(defaultSpacing), + ), + Semantics( + label: 'clear allocated objects', + button: true, + child: ShadButton.destructive( + onPressed: _clearObjects, + child: const Text('Clear Allocated Objects'), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/network/http_client.dart b/devexp_demos/devtools_companion/lib/src/screens/network/http_client.dart new file mode 100644 index 0000000..08a681f --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/network/http_client.dart @@ -0,0 +1,354 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:dio/dio.dart'; +import 'package:http/http.dart' as http; + +import 'http_server.dart'; + +/// The options for what the process should be after a request is sent; whether +/// the request should be completed by the server, or not, or cancelled by the +/// client. +enum CompletionType { + completes('Completes'), + doesNotComplete('Does not complete'), + isCancelled('Is cancelled before completes'); + + const CompletionType(this.text); + + final String text; +} + +/// The HTTP client code that makes HTTP requests and logs various things. +class HttpClient { + final io.HttpClient _client = io.HttpClient(); + + final _dio = Dio(); + + /// Sends an HTTP GET request using `dart:io`, and awaits a response. + void get({ + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _ioRequest( + method: 'GET', + networkNotifier: networkNotifier, + requestHasBody: requestHasBody, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + openRequest: (uri) => _client.getUrl(uri), + ); + } + + /// Sends an HTTP POST request using `dart:io`, and awaits a response. + void post({ + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _ioRequest( + method: 'POST', + networkNotifier: networkNotifier, + requestHasBody: requestHasBody, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + openRequest: (uri) => _client.postUrl(uri), + ); + } + + /// Sends an HTTP PUT request using `dart:io`, and awaits a response. + void put({ + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _ioRequest( + method: 'PUT', + networkNotifier: networkNotifier, + requestHasBody: requestHasBody, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + openRequest: (uri) => _client.putUrl(uri), + ); + } + + /// Sends an HTTP DELETE request using `dart:io`, and awaits a response. + void delete({ + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _ioRequest( + method: 'DELETE', + networkNotifier: networkNotifier, + requestHasBody: requestHasBody, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + openRequest: (uri) => _client.deleteUrl(uri), + ); + } + + /// Sends a request with the dart:io library as per the parameters. + void _ioRequest({ + required String method, + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + required CompletionType completionType, + required Future Function(Uri) openRequest, + }) async { + final uri = _computeUri( + responseHasBody: responseHasBody, + completionType: completionType, + responseCode: responseCode, + ); + networkNotifier('Sending $method to $uri...'); + final request = await openRequest(uri); + networkNotifier('Sent $method.'); + if (requestHasBody) { + request.write('Request Body'); + } + if (completionType == CompletionType.isCancelled) { + Timer(const Duration(seconds: 1), () { + networkNotifier('$method aborted.'); + request.abort(); + }); + } + try { + final response = await request.close(); + networkNotifier('Received $method response: $response'); + } catch (e) { + networkNotifier('$method caught exception: $e'); + } + } + + /// Sends an HTTP GET request using the http package, and awaits a response. + void packageHttpGet({ + required Logger networkNotifier, + // Unused. + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _packageHttpRequest( + method: 'GET', + networkNotifier: networkNotifier, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + action: (client, uri) => client.get(uri), + ); + } + + /// Sends an HTTP POST request using the http package, and awaits a response. + void packageHttpPost({ + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _packageHttpRequest( + method: 'POST', + networkNotifier: networkNotifier, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + action: (client, uri) => client.post( + uri, + body: requestHasBody ? {'name': 'doodle', 'color': 'blue'} : null, + ), + ); + } + + /// Sends a streamed HTTP POST request using the http package, and awaits a + /// response. + void packageHttpPostStreamed({ + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _packageHttpRequest( + method: 'streamed POST', + networkNotifier: networkNotifier, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + action: (client, uri) async { + final request = http.StreamedRequest('POST', uri) + ..contentLength = 20 + ..sink.add([11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) + ..sink.add([21, 22, 23, 24, 25, 26, 27, 28, 29, 30]); + await request.sink.close(); + return client.send(request); + }, + ); + } + + /// Sends an HTTP DELETE request using the http package, and awaits a + /// response. + void packageHttpDelete({ + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _packageHttpRequest( + method: 'DELETE', + networkNotifier: networkNotifier, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + action: (client, uri) => client.delete( + uri, + body: requestHasBody ? {'name': 'doodle', 'color': 'blue'} : null, + ), + ); + } + + /// Sends a request with the http package as per the parameters. + void _packageHttpRequest({ + required String method, + required Logger networkNotifier, + required bool responseHasBody, + required int responseCode, + required CompletionType completionType, + required Future Function(http.Client, Uri) action, + }) async { + final uri = _computeUri( + responseHasBody: responseHasBody, + completionType: completionType, + responseCode: responseCode, + ); + networkNotifier('Sending package:http $method to $uri...'); + final client = http.Client(); + if (completionType == CompletionType.isCancelled) { + Timer(const Duration(seconds: 1), () { + networkNotifier('package:http $method client closed.'); + client.close(); + }); + } + try { + final response = await action(client, uri); + networkNotifier('Received package:http $method response: $response'); + } catch (e) { + networkNotifier('package:http $method caught exception: $e'); + } finally { + client.close(); + } + } + + void dioGet({ + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _dioRequest( + method: 'GET', + networkNotifier: networkNotifier, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + action: (cancelToken, uri) => _dio.getUri(uri, cancelToken: cancelToken), + ); + } + + void dioPost({ + required Logger networkNotifier, + required bool requestHasBody, + required bool responseHasBody, + required int responseCode, + CompletionType completionType = CompletionType.completes, + }) { + _dioRequest( + method: 'POST', + networkNotifier: networkNotifier, + responseHasBody: responseHasBody, + responseCode: responseCode, + completionType: completionType, + action: (cancelToken, uri) => _dio.postUri( + uri, + data: requestHasBody ? {'a': 'b', 'c': 'd'} : null, + cancelToken: cancelToken, + ), + ); + } + + /// Sends a request with the dio package as per the parameters. + void _dioRequest({ + required String method, + required Logger networkNotifier, + required bool responseHasBody, + required int responseCode, + required CompletionType completionType, + required Future Function(CancelToken, Uri) action, + }) async { + final uri = _computeUri( + responseHasBody: responseHasBody, + completionType: completionType, + responseCode: responseCode, + ); + networkNotifier('Sending Dio $method to $uri...'); + final cancelToken = CancelToken(); + if (completionType == CompletionType.isCancelled) { + Timer(const Duration(seconds: 1), () { + networkNotifier('Dio $method cancelled.'); + cancelToken.cancel('User cancelled request'); + }); + } + try { + final response = await action(cancelToken, uri); + networkNotifier( + 'Recived Dio $method response; headers: ${response.headers}', + ); + } on DioException catch (e) { + if (CancelToken.isCancel(e)) { + networkNotifier('Dio $method caught cancellation: ${e.message}'); + } else { + networkNotifier('Dio $method caught DioException: $e'); + } + } catch (e) { + networkNotifier('Dio $method caught exception: $e'); + } + } + + /// Computes a [Uri] from various configuration. + /// + /// The configuration is embedded into the URI, conveying the configuration to + /// the HTTP server. + Uri _computeUri({ + required bool responseHasBody, + required CompletionType completionType, + required int responseCode, + }) => Uri.http( + '127.0.0.1:$httpServerPort', + [ + '/', + if (responseHasBody) 'responseHasBody/', + if (completionType == CompletionType.completes) 'complete/', + ].join(), + {'responseCode': '$responseCode'}, + ); +} + +typedef Logger = void Function(String); diff --git a/devexp_demos/devtools_companion/lib/src/screens/network/http_server.dart b/devexp_demos/devtools_companion/lib/src/screens/network/http_server.dart new file mode 100644 index 0000000..f472437 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/network/http_server.dart @@ -0,0 +1,45 @@ +import 'dart:io' as io; + +/// The HttpServer used for all HTTP requests. +class HttpServer { + const HttpServer(this._server); + + final io.HttpServer _server; + + static Future create() async { + final ioServer = await io.HttpServer.bind( + io.InternetAddress.loopbackIPv4, + httpServerPort, + ); + + ioServer.listen((request) async { + final path = request.uri.path; + final queryParameters = request.uri.queryParameters; + final responseCode = + int.tryParse(queryParameters['responseCode'] ?? '200') ?? 200; + request.response.statusCode = responseCode; + if (path.contains('responseHasBody/')) { + request.response.headers.contentType = io.ContentType( + 'application', + 'json', + charset: 'utf-8', + ); + request.response.write('{"response body":7}'); + await request.response.flush(); + } + if (path.contains('complete/')) { + // Wait 3 seconds before completing the response, in order to allow a + // request to be cancelled. + await Future.delayed(const Duration(seconds: 3)); + await request.response.close(); + } + }); + + return HttpServer(ioServer); + } + + /// The port that the [io.HttpServer] ultimately bound to. + int get port => _server.port; +} + +const httpServerPort = 8888; diff --git a/devexp_demos/devtools_companion/lib/src/screens/network/network_screen.dart b/devexp_demos/devtools_companion/lib/src/screens/network/network_screen.dart new file mode 100644 index 0000000..8b7c378 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/network/network_screen.dart @@ -0,0 +1,361 @@ +import 'dart:async'; + +import 'package:flutter/material.dart' hide Checkbox, TextFormField; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../shared/ui/theme.dart'; +import 'http_client.dart'; +import 'http_server.dart'; + +class NetworkScreen extends StatefulWidget { + const NetworkScreen({super.key}); + + @override + State createState() => _NetworkScreenState(); +} + +class _NetworkScreenState extends State { + final _httpClient = HttpClient(); + + final Future _server = HttpServer.create(); + + @override + Widget build(BuildContext context) { + // Initialize the global HTTP server. + unawaited(_server); + return SingleChildScrollView( + child: RequestTable( + httpClient: _httpClient, + networkNotifier: _networkNotifier, + ), + ); + } + + void _networkNotifier(String text) { + final toast = ShadToast( + alignment: Alignment.topCenter, + description: Text(text), + backgroundColor: ShadTheme.of(context).colorScheme.accent, + ); + ShadToaster.of(context).show(toast); + } +} + +class _RequestSettings { + _RequestSettings({ + required this.type, + required this.action, + this.requestHasBody = false, + this.requestCanHaveBody = true, + }); + + final _RequestType type; + final void Function({ + required Logger networkNotifier, + required bool requestHasBody, + required int responseCode, + required bool responseHasBody, + CompletionType completionType, + }) + action; + + /// `null` means disabled. + bool? requestHasBody; + bool requestCanHaveBody; + int responseCode = 200; + bool responseHasBody = true; + CompletionType completionType = CompletionType.completes; + bool shouldRepeat = false; +} + +enum _RequestType { + httpGet('dart:io GET'), + httpPost('dart:io POST'), + httpPut('dart:io PUT'), + httpDelete('dart:io DELETE'), + packageHttpGet('package:http GET'), + packageHttpPost('package:http POST'), + packageHttpPostStreamed('package:http POST (streamed)'), + packageHttpDelete('package:http DELETE'), + dioGet('Dio GET'), + dioPost('Dio POST'); + // TODO: WebSocket + // TODO: cronet_http - https://pub.dev/packages/cronet_http + // TODO: ok_http - https://pub.dev/packages/ok_http + + const _RequestType(this.text); + + final String text; +} + +typedef Logger = void Function(String); + +class RequestTable extends StatefulWidget { + const RequestTable({ + required HttpClient httpClient, + required Logger networkNotifier, + super.key, + }) : _httpClient = httpClient, + _networkNotifier = networkNotifier; + + final HttpClient _httpClient; + final Logger _networkNotifier; + + @override + State createState() => _RequestTableState(); +} + +class _RequestTableState extends State { + final _repeatingTimers = <_RequestSettings, Timer>{}; + + @override + void dispose() { + for (final timer in _repeatingTimers.values) { + timer.cancel(); + } + super.dispose(); + } + + late List<_RequestSettings> settingsList = [ + _RequestSettings( + type: _RequestType.httpGet, + action: widget._httpClient.get, + requestHasBody: null, + requestCanHaveBody: false, + ), + _RequestSettings( + type: _RequestType.httpPost, + action: widget._httpClient.post, + ), + _RequestSettings( + type: _RequestType.httpPut, + action: widget._httpClient.put, + ), + _RequestSettings( + type: _RequestType.httpDelete, + action: widget._httpClient.delete, + requestHasBody: null, + requestCanHaveBody: false, + ), + _RequestSettings( + type: _RequestType.packageHttpGet, + action: widget._httpClient.packageHttpGet, + ), + _RequestSettings( + type: _RequestType.packageHttpPost, + action: widget._httpClient.packageHttpPost, + ), + _RequestSettings( + type: _RequestType.packageHttpPostStreamed, + action: widget._httpClient.packageHttpPostStreamed, + requestHasBody: null, + ), + _RequestSettings( + type: _RequestType.packageHttpDelete, + action: widget._httpClient.packageHttpDelete, + ), + _RequestSettings( + type: _RequestType.dioGet, + action: widget._httpClient.dioGet, + ), + _RequestSettings( + type: _RequestType.dioPost, + action: widget._httpClient.dioPost, + ), + ]; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + for (var i = 0; i < settingsList.length; i++) ...[ + () { + final settings = settingsList[i]; + return ShadCard( + child: Padding( + padding: const EdgeInsets.all(densePadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + settings.type.text, + style: ShadTheme.of(context).textTheme.h4, + ), + ), + Row( + children: [ + Text( + 'Code: ', + style: ShadTheme.of(context).textTheme.p, + ), + SizedBox( + width: 60, + child: ShadInput( + initialValue: '200', + keyboardType: TextInputType.number, + padding: const EdgeInsets.symmetric( + horizontal: defaultPadding, + vertical: densePadding, + ), + onChanged: (value) { + setState(() { + settings.responseCode = + int.tryParse(value) ?? 200; + }); + }, + ), + ), + ], + ), + ], + ), + const SizedBox(height: denseSpacing), + const Divider(), + GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + childAspectRatio: 4.5, + children: [ + _buildCheckboxRow( + label: 'Request Body', + value: + settings.requestHasBody ?? + settings.requestCanHaveBody, + enabled: settings.requestHasBody != null, + onChanged: (val) => + setState(() => settings.requestHasBody = val), + ), + _buildCheckboxRow( + label: 'Response Body', + value: settings.responseHasBody, + onChanged: (val) => + setState(() => settings.responseHasBody = val), + ), + _buildCheckboxRow( + label: 'Repeats', + value: settings.shouldRepeat, + onChanged: (val) => + setState(() => settings.shouldRepeat = val), + ), + ], + ), + const SizedBox(height: denseSpacing), + Row( + children: [ + Text( + 'Completion: ', + style: ShadTheme.of(context).textTheme.small, + ), + Expanded( + child: ShadSelect( + initialValue: settings.completionType, + onChanged: (val) => setState( + () => settings.completionType = val!, + ), + options: + CompletionType.values + .map( + (e) => ShadOption( + value: e, + child: Text(e.text), + ), + ) + .toList(), + selectedOptionBuilder: + (context, value) => Text(value.text), + ), + ), + ], + ), + const SizedBox(height: denseSpacing), + SizedBox( + width: double.infinity, + child: ShadButton( + onPressed: () => _handleAction(settings), + child: Text( + settings.shouldRepeat + ? (_repeatingTimers.containsKey(settings) + ? 'Stop' + : 'Start Loop') + : 'Send Request', + ), + ), + ), + ], + ), + ), + ); + }(), + if (i < settingsList.length - 1) + const SizedBox(height: largeSpacing), + ], + ], + ), + ); + } + + Widget _buildCheckboxRow({ + required String label, + required bool value, + required Function(bool) onChanged, + bool enabled = true, + }) { + return Row( + children: [ + ShadCheckbox(value: value, onChanged: enabled ? onChanged : null), + const SizedBox(width: 8), + Expanded( + child: Text( + label, + style: TextStyle( + color: enabled ? null : Theme.of(context).disabledColor, + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + + void _handleAction(_RequestSettings settings) { + if (settings.shouldRepeat) { + if (_repeatingTimers.containsKey(settings)) { + // Stop the timer. + _repeatingTimers[settings]!.cancel(); + setState(() { + _repeatingTimers.remove(settings); + }); + } else { + // Start the timer. + final timer = Timer.periodic( + const Duration(seconds: 1), + (timer) => _runAction(settings), + ); + setState(() { + _repeatingTimers[settings] = timer; + }); + } + } else { + // Just run once. + _runAction(settings); + } + } + + void _runAction(_RequestSettings settings) { + settings.action( + networkNotifier: widget._networkNotifier, + requestHasBody: settings.requestHasBody ?? false, + responseCode: settings.responseCode, + responseHasBody: settings.responseHasBody, + completionType: settings.completionType, + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/performance/data/fruits.dart b/devexp_demos/devtools_companion/lib/src/screens/performance/data/fruits.dart new file mode 100644 index 0000000..039d79f --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/performance/data/fruits.dart @@ -0,0 +1,145 @@ +import 'dart:convert'; + +class Fruit { + const Fruit({required this.title, required this.imageUrl}); + + factory Fruit.fromJson(Map json) { + return Fruit( + title: json['title'] as String, + imageUrl: json['image_path'] as String, + ); + } + + static List parseJson(String jsonStr) { + final json = jsonDecode(jsonStr) as List; + return json.map((item) => Fruit.fromJson(item)).toList(); + } + + final String title; + final String imageUrl; +} + +const fruitDataStr = ''' +[ + { + "title": "Apple", + "image_path": "assets/images/apple.jpg" + }, + { + "title": "Artichoke", + "image_path": "assets/images/artichoke.jpg" + }, + { + "title": "Asparagus", + "image_path": "assets/images/asparagus.jpg" + }, + { + "title": "Avocado", + "image_path": "assets/images/avocado.jpg" + }, + { + "title": "Blackberry", + "image_path": "assets/images/blackberry.jpg" + }, + { + "title": "Cantaloupe", + "image_path": "assets/images/cantaloupe.jpg" + }, + { + "title": "Cauliflower", + "image_path": "assets/images/cauliflower.jpg" + }, + { + "title": "Endive", + "image_path": "assets/images/endive.jpg" + }, + { + "title": "Fig", + "image_path": "assets/images/fig.jpg" + }, + { + "title": "Grape", + "image_path": "assets/images/grape.jpg" + }, + { + "title": "Green Bell Pepper", + "image_path": "assets/images/green_bell_pepper.jpg" + }, + { + "title": "Habanero", + "image_path": "assets/images/habanero.jpg" + }, + { + "title": "Kale", + "image_path": "assets/images/kale.jpg" + }, + { + "title": "Kiwi", + "image_path": "assets/images/kiwi.jpg" + }, + { + "title": "Lemon", + "image_path": "assets/images/lemon.jpg" + }, + { + "title": "Lime", + "image_path": "assets/images/lime.jpg" + }, + { + "title": "Mango", + "image_path": "assets/images/mango.jpg" + }, + { + "title": "Mushroom", + "image_path": "assets/images/mushroom.jpg" + }, + { + "title": "Nectarine", + "image_path": "assets/images/nectarine.jpg" + }, + { + "title": "Persimmon", + "image_path": "assets/images/persimmon.jpg" + }, + { + "title": "Plum", + "image_path": "assets/images/plum.jpg" + }, + { + "title": "Potato", + "image_path": "assets/images/potato.jpg" + }, + { + "title": "Radicchio", + "image_path": "assets/images/radicchio.jpg" + }, + { + "title": "Radish", + "image_path": "assets/images/radish.jpg" + }, + { + "title": "Squash", + "image_path": "assets/images/squash.jpg" + }, + { + "title": "Strawberry", + "image_path": "assets/images/strawberry.jpg" + }, + { + "title": "Tangelo", + "image_path": "assets/images/tangelo.jpg" + }, + { + "title": "Tomato", + "image_path": "assets/images/tomato.jpg" + }, + { + "title": "Watermelon", + "image_path": "assets/images/watermelon.jpg" + }, + { + "title": "Orange Bell Pepper", + "image_path": "assets/images/orange_bell_pepper.jpg" + } +] +'''; diff --git a/devexp_demos/devtools_companion/lib/src/screens/performance/fruit_details.dart b/devexp_demos/devtools_companion/lib/src/screens/performance/fruit_details.dart new file mode 100644 index 0000000..cdfcb71 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/performance/fruit_details.dart @@ -0,0 +1,34 @@ +import 'package:flutter/widgets.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../shared/ui/theme.dart'; +import 'data/fruits.dart'; + +class FruitDetailsScreen extends StatelessWidget { + const FruitDetailsScreen({super.key, required this.fruit}); + + final Fruit fruit; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: ShadCard( + backgroundColor: theme.colorScheme.secondary, + padding: const EdgeInsets.all(noPadding), + footer: Padding( + padding: const EdgeInsets.symmetric(vertical: densePadding), + child: Center( + child: Text( + fruit.title, + style: theme.textTheme.p.copyWith(fontStyle: FontStyle.italic), + ), + ), + ), + child: Image.asset(fruit.imageUrl), + ), + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/performance/fruit_list.dart b/devexp_demos/devtools_companion/lib/src/screens/performance/fruit_list.dart new file mode 100644 index 0000000..95e36f4 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/performance/fruit_list.dart @@ -0,0 +1,89 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../scaffold/router.dart'; +import '../../shared/ui/theme.dart'; +import 'data/fruits.dart'; + +class FruitsList extends StatefulWidget { + const FruitsList({super.key}); + + @override + State createState() => _FruitsListState(); +} + +class _FruitsListState extends State { + late Future> _fruitData; + + @override + void initState() { + super.initState(); + _fruitData = Future.value(Fruit.parseJson(fruitDataStr)); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _fruitData, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + if (snapshot.hasData) { + return _FruitTiles(fruitData: snapshot.data!); + } + return const Center(child: Text('No fruits found.')); + }, + ); + } +} + +class _FruitTiles extends StatelessWidget { + const _FruitTiles({required this.fruitData}); + + final List fruitData; + + static const _imageSize = 60.0; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: fruitData.length * 3, + itemExtent: _imageSize + defaultSpacing, + itemBuilder: (context, index) { + final fruit = fruitData[index % fruitData.length]; + return Padding( + padding: const EdgeInsets.all(densePadding), + child: ListTile( + onTap: () => Navigator.of( + context, + ).pushNamed(AppRoute.performanceDetails.path, arguments: fruit), + leading: ClipRRect( + borderRadius: BorderRadius.circular(4.0), + child: Image.asset( + fruit.imageUrl, + width: _imageSize, + height: _imageSize, + cacheWidth: 150, + cacheHeight: 150, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: _imageSize, + height: _imageSize, + color: Colors.grey[200], + child: const Icon(Icons.broken_image, color: Colors.grey), + ); + }, + ), + ), + title: Text(fruit.title), + ), + ); + }, + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/performance/performance_controller.dart b/devexp_demos/devtools_companion/lib/src/screens/performance/performance_controller.dart new file mode 100644 index 0000000..703a2fc --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/performance/performance_controller.dart @@ -0,0 +1,12 @@ +import 'package:flutter/widgets.dart'; + +class PerformanceScreenController { + PerformanceScreenController(); + final uiJankDurationMs = ValueNotifier(0.0); + final rasterJankIntensity = ValueNotifier(0.0); + + void dispose() { + uiJankDurationMs.dispose(); + rasterJankIntensity.dispose(); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/performance/performance_saboteur.dart b/devexp_demos/devtools_companion/lib/src/screens/performance/performance_saboteur.dart new file mode 100644 index 0000000..9a33099 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/performance/performance_saboteur.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class RasterThreadSaboteur extends StatelessWidget { + const RasterThreadSaboteur({super.key, required this.intensity}); + + final int intensity; + + @override + Widget build(BuildContext context) { + if (intensity == 0) return const SizedBox.shrink(); + + Widget child = const SizedBox.shrink(); + + for (int i = 0; i < intensity * 5; i++) { + child = Stack( + fit: StackFit.expand, + children: [ + child, + // Opacity + Clip + Blur = Expensive Rasterization + Opacity( + opacity: 0.01, + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container(color: Colors.white.withAlpha(10)), + ), + ), + ), + ], + ); + } + + // Make it invisible. + return Positioned.fill(child: IgnorePointer(child: child)); + } +} + +class UiThreadSaboteur extends StatefulWidget { + const UiThreadSaboteur({super.key, required this.jankMs}); + final int jankMs; + + @override + State createState() => _UiThreadSaboteurState(); +} + +class _UiThreadSaboteurState extends State + with SingleTickerProviderStateMixin { + late final Ticker _ticker; + + @override + void initState() { + super.initState(); + _ticker = createTicker((_) => _induceJank()); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateTickerState(); + } + + @override + void didUpdateWidget(covariant UiThreadSaboteur oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.jankMs != widget.jankMs) { + _updateTickerState(); + } + } + + void _updateTickerState() { + final shouldRun = TickerMode.of(context) && widget.jankMs > 0; + if (shouldRun && !_ticker.isActive) { + unawaited(_ticker.start()); + } else if (!shouldRun && _ticker.isActive) { + _ticker.stop(); + } + } + + void _induceJank() { + final stallMs = widget.jankMs; + if (stallMs <= 0) return; + + final stopwatch = Stopwatch()..start(); + while (stopwatch.elapsedMilliseconds < stallMs) { + // This busy-wait loop intentionally blocks the UI thread to induce jank. + } + } + + @override + void dispose() { + _ticker.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => const SizedBox.shrink(); +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/performance/performance_screen.dart b/devexp_demos/devtools_companion/lib/src/screens/performance/performance_screen.dart new file mode 100644 index 0000000..c7a27b1 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/performance/performance_screen.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../shared/ui/theme.dart'; +import 'fruit_list.dart'; +import 'performance_controller.dart'; +import 'performance_saboteur.dart'; +import 'performance_settings.dart'; + +class PerformanceScreen extends StatefulWidget { + const PerformanceScreen({super.key}); + + @override + State createState() => _PerformanceScreenState(); +} + +class _PerformanceScreenState extends State { + late final PerformanceScreenController _controller; + + @override + void initState() { + super.initState(); + _controller = PerformanceScreenController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: defaultPadding), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text('Fruits & Veggies', style: theme.textTheme.h2), + JankSettingsPopover(controller: _controller), + ], + ), + ), + const Expanded(child: FruitsList()), + ], + ), + ValueListenableBuilder( + valueListenable: _controller.rasterJankIntensity, + builder: (context, value, child) => + RasterThreadSaboteur(intensity: value.round()), + ), + ValueListenableBuilder( + valueListenable: _controller.uiJankDurationMs, + builder: (context, value, child) => + UiThreadSaboteur(jankMs: value.round()), + ), + ], + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/performance/performance_settings.dart b/devexp_demos/devtools_companion/lib/src/screens/performance/performance_settings.dart new file mode 100644 index 0000000..2c83063 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/performance/performance_settings.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../shared/ui/theme.dart'; +import 'performance_controller.dart'; + +class JankSettingsPopover extends StatefulWidget { + const JankSettingsPopover({super.key, required this.controller}); + + final PerformanceScreenController controller; + @override + State createState() => _JankSettingsPopoverState(); +} + +class _JankSettingsPopoverState extends State { + final popoverController = ShadPopoverController(); + + @override + void dispose() { + popoverController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ShadPopover( + controller: popoverController, + popover: (context) => JankSettings( + popoverController: popoverController, + controller: widget.controller, + ), + child: ShadButton.outline( + onPressed: popoverController.toggle, + child: const Text('Jank Controls'), + ), + ); + } +} + +class JankSettings extends StatefulWidget { + const JankSettings({ + super.key, + required this.popoverController, + required this.controller, + }); + + final ShadPopoverController popoverController; + final PerformanceScreenController controller; + + @override + State createState() => _JankSettingsState(); +} + +class _JankSettingsState extends State { + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 350), + child: Padding( + padding: const EdgeInsets.only(top: defaultSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('UI Jank:'), + ValueListenableBuilder( + valueListenable: widget.controller.uiJankDurationMs, + builder: (context, value, child) { + return Slider( + value: value, + max: 50, + divisions: 10, + label: 'UI Jank Stall (${value.round()}ms)', + onChanged: (v) => + widget.controller.uiJankDurationMs.value = v, + ); + }, + ), + const Text('Raster Jank:'), + ValueListenableBuilder( + valueListenable: widget.controller.rasterJankIntensity, + builder: (context, value, child) { + return Slider( + value: value, + max: 10, + divisions: 10, + label: 'Raster Jank Intensity (${value.round()})', + onChanged: (v) => + widget.controller.rasterJankIntensity.value = v, + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/screens/timeline/timeline_screen.dart b/devexp_demos/devtools_companion/lib/src/screens/timeline/timeline_screen.dart new file mode 100644 index 0000000..64bf0bc --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/screens/timeline/timeline_screen.dart @@ -0,0 +1,58 @@ +import 'dart:developer'; + +import 'package:flutter/widgets.dart'; + +import '../../shared/ui/theme.dart'; +import '../../shared/widgets/expensive_task_widget.dart'; + +class TimelineScreen extends StatelessWidget { + const TimelineScreen({super.key}); + + @override + Widget build(BuildContext context) { + return ListView( + padding: const .all(largePadding), + children: [ + ExpensiveTaskWidget( + title: 'Compute Fibonacci (32)', + task: () => _fib(32).toString(), + ), + ExpensiveTaskWidget( + title: 'Compute Fibonacci async (24)', + task: () async => (await _fibAsync(24)).toString(), + ), + // TODO: Something with `Timeline.instant`. + // TODO: Something with passing a TimelineTask to a different Isolate. + ], + ); + } +} + +Future _fibAsync(int n, {TimelineTask? parent}) async { + final task = TimelineTask(parent: parent)..start('Fib $n'); + if (n <= 1) { + // TODO: Something with `arguments` here? Do they show up in DevTools? + task.finish(); + return n; + } + await Future.delayed(const Duration(milliseconds: 16)); + final (a, b) = await ( + _fibAsync(n - 1, parent: task), + _fibAsync(n - 2, parent: task), + ).wait; + final result = a + b; + + task.finish(); + return result; +} + +int _fib(int n) { + Timeline.startSync('Fib $n'); + if (n <= 1) { + Timeline.finishSync(); + return n; + } + final result = _fib(n - 1) + _fib(n - 2); + Timeline.finishSync(); + return result; +} diff --git a/devexp_demos/devtools_companion/lib/src/shared/ui/theme.dart b/devexp_demos/devtools_companion/lib/src/shared/ui/theme.dart new file mode 100644 index 0000000..1295568 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/shared/ui/theme.dart @@ -0,0 +1,11 @@ +const extraLargeSpacing = 32.0; +const largeSpacing = 16.0; +const defaultSpacing = 12.0; +const intermediateSpacing = 10.0; +const denseSpacing = 8.0; + +const extraLargePadding = 10.0; +const largePadding = 8.0; +const defaultPadding = 6.0; +const densePadding = 4.0; +const noPadding = 0.0; diff --git a/devexp_demos/devtools_companion/lib/src/shared/utils/utils.dart b/devexp_demos/devtools_companion/lib/src/shared/utils/utils.dart new file mode 100644 index 0000000..9fe1a3b --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/shared/utils/utils.dart @@ -0,0 +1 @@ +// Add any utility functions here. \ No newline at end of file diff --git a/devexp_demos/devtools_companion/lib/src/shared/widgets/expensive_task_widget.dart b/devexp_demos/devtools_companion/lib/src/shared/widgets/expensive_task_widget.dart new file mode 100644 index 0000000..0e48948 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/shared/widgets/expensive_task_widget.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../ui/theme.dart'; + +class ExpensiveTaskWidget extends StatefulWidget { + const ExpensiveTaskWidget({ + super.key, + required this.title, + required this.task, + this.children, + }); + + final String title; + final FutureOr Function() task; + final List? children; + + @override + State createState() => _ExpensiveTaskWidgetState(); +} + +class _ExpensiveTaskWidgetState extends State { + bool _isRunning = false; + String? _result; + Duration? _executionTime; + + Future _runTask() async { + setState(() { + _isRunning = true; + _result = null; + _executionTime = null; + }); + + // Allow the UI to update to the loading state before blocking + await Future.delayed(const Duration(milliseconds: 50)); + + final stopwatch = Stopwatch()..start(); + try { + final result = await widget.task(); + stopwatch.stop(); + if (mounted) { + setState(() { + _result = result; + _executionTime = stopwatch.elapsed; + }); + } + } catch (e) { + stopwatch.stop(); + if (mounted) { + setState(() { + _result = 'Error: $e'; + _executionTime = stopwatch.elapsed; + }); + } + } finally { + if (mounted) { + setState(() { + _isRunning = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Card( + margin: const .all(denseSpacing), + child: Padding( + padding: const .all(largePadding), + child: Column( + crossAxisAlignment: .start, + children: [ + Text(widget.title, style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: denseSpacing), + Row( + children: [ + ShadIconButton( + icon: _isRunning + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Icon(Icons.play_arrow), + onPressed: _runTask, + enabled: !_isRunning, + ), + const SizedBox(width: largeSpacing), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_executionTime != null) + Text( + 'Duration: ${_executionTime!.inMilliseconds}ms', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + if (_result != null) + Text( + 'Result: $_result', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + if (widget.children != null) ...[Row(children: widget.children!)], + ], + ), + ), + ); + } +} diff --git a/devexp_demos/devtools_companion/lib/src/shared/widgets/todo_screen.dart b/devexp_demos/devtools_companion/lib/src/shared/widgets/todo_screen.dart new file mode 100644 index 0000000..8344db5 --- /dev/null +++ b/devexp_demos/devtools_companion/lib/src/shared/widgets/todo_screen.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../shared/ui/theme.dart'; + +class TodoScreen extends StatelessWidget { + const TodoScreen({super.key, required this.screenName}); + + final String screenName; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final isDarkMode = theme.brightness == Brightness.dark; + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(denseSpacing), + child: ShadCard( + child: Text.rich( + TextSpan( + style: theme.textTheme.p, + children: [ + TextSpan( + text: 'TODO:', + style: theme.textTheme.p.copyWith( + backgroundColor: isDarkMode + ? Colors.purple + : Colors.yellow, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + ' Modify this screen to trigger useful actions for developers working on the DevTools $screenName panel.\n\n', + ), + const TextSpan( + text: 'For style consistency, please use widgets from ', + style: TextStyle(fontStyle: FontStyle.italic), + ), + const TextSpan( + text: 'package:shadcn_ui', + style: TextStyle(fontFamily: 'RobotoMono'), + ), + const TextSpan( + text: ' whenever possible.\n', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: extraLargeSpacing), + child: ShadButton.outline( + child: const Text('Click me!'), + onPressed: () { + ShadToaster.of(context).show( + ShadToast( + description: Text('Welcome to the companion tool for the DevTools $screenName Panel!'), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/devexp_demos/devtools_companion/linux/.gitignore b/devexp_demos/devtools_companion/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/devexp_demos/devtools_companion/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/devexp_demos/devtools_companion/linux/CMakeLists.txt b/devexp_demos/devtools_companion/linux/CMakeLists.txt new file mode 100644 index 0000000..da2dca3 --- /dev/null +++ b/devexp_demos/devtools_companion/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "devtools_companion") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.devtools_companion") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/devexp_demos/devtools_companion/linux/flutter/CMakeLists.txt b/devexp_demos/devtools_companion/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/devexp_demos/devtools_companion/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/devexp_demos/devtools_companion/linux/flutter/generated_plugin_registrant.cc b/devexp_demos/devtools_companion/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/devexp_demos/devtools_companion/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/devexp_demos/devtools_companion/linux/flutter/generated_plugin_registrant.h b/devexp_demos/devtools_companion/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/devexp_demos/devtools_companion/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/devexp_demos/devtools_companion/linux/flutter/generated_plugins.cmake b/devexp_demos/devtools_companion/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/devexp_demos/devtools_companion/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/devexp_demos/devtools_companion/linux/runner/CMakeLists.txt b/devexp_demos/devtools_companion/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/devexp_demos/devtools_companion/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/devexp_demos/devtools_companion/linux/runner/main.cc b/devexp_demos/devtools_companion/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/devexp_demos/devtools_companion/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/devexp_demos/devtools_companion/linux/runner/my_application.cc b/devexp_demos/devtools_companion/linux/runner/my_application.cc new file mode 100644 index 0000000..8dc54d5 --- /dev/null +++ b/devexp_demos/devtools_companion/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "devtools_companion"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "devtools_companion"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/devexp_demos/devtools_companion/linux/runner/my_application.h b/devexp_demos/devtools_companion/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/devexp_demos/devtools_companion/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/devexp_demos/devtools_companion/macos/.gitignore b/devexp_demos/devtools_companion/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/devexp_demos/devtools_companion/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/devexp_demos/devtools_companion/macos/Flutter/Flutter-Debug.xcconfig b/devexp_demos/devtools_companion/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/devexp_demos/devtools_companion/macos/Flutter/Flutter-Release.xcconfig b/devexp_demos/devtools_companion/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/devexp_demos/devtools_companion/macos/Flutter/GeneratedPluginRegistrant.swift b/devexp_demos/devtools_companion/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..e777c67 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import path_provider_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) +} diff --git a/devexp_demos/devtools_companion/macos/Podfile b/devexp_demos/devtools_companion/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_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_macos_build_settings(target) + end +end diff --git a/devexp_demos/devtools_companion/macos/Runner.xcodeproj/project.pbxproj b/devexp_demos/devtools_companion/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4d23f6f --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,807 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 6F413FAD4AA73560467A1A2D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9CF4E20C67BF90C5F53E614B /* Pods_Runner.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + DFB82DFA499226169AE0FD1D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30E6D50257FB0BE25A1B500F /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0025F80803E006A46F7D5A60 /* 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 = ""; }; + 1154510954E456E256FCC9AC /* 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 = ""; }; + 30E6D50257FB0BE25A1B500F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* devtools_companion.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = devtools_companion.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7086B220ED5F2794E269BB41 /* 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 = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 916D1A9B38CB0017899912E3 /* 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 = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9CF4E20C67BF90C5F53E614B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D86AE9CE263C4829CDC29CED /* 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 = ""; }; + D9189555FB6531528618687C /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DFB82DFA499226169AE0FD1D /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 6F413FAD4AA73560467A1A2D /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2DC258FF76B4803FB11B58EB /* Pods */ = { + isa = PBXGroup; + children = ( + D86AE9CE263C4829CDC29CED /* Pods-Runner.debug.xcconfig */, + 7086B220ED5F2794E269BB41 /* Pods-Runner.release.xcconfig */, + 0025F80803E006A46F7D5A60 /* Pods-Runner.profile.xcconfig */, + 916D1A9B38CB0017899912E3 /* Pods-RunnerTests.debug.xcconfig */, + D9189555FB6531528618687C /* Pods-RunnerTests.release.xcconfig */, + 1154510954E456E256FCC9AC /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 2DC258FF76B4803FB11B58EB /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* devtools_companion.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9CF4E20C67BF90C5F53E614B /* Pods_Runner.framework */, + 30E6D50257FB0BE25A1B500F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 6583B404D2E556EFF5EA0925 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 32737C9057EE657B5175BDE4 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* devtools_companion.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 32737C9057EE657B5175BDE4 /* [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; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 6583B404D2E556EFF5EA0925 /* [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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 916D1A9B38CB0017899912E3 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/devtools_companion.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/devtools_companion"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D9189555FB6531528618687C /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/devtools_companion.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/devtools_companion"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1154510954E456E256FCC9AC /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/devtools_companion.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/devtools_companion"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/devexp_demos/devtools_companion/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/devexp_demos/devtools_companion/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/devexp_demos/devtools_companion/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/devexp_demos/devtools_companion/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a7fc98c --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/devexp_demos/devtools_companion/macos/Runner.xcworkspace/contents.xcworkspacedata b/devexp_demos/devtools_companion/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/devexp_demos/devtools_companion/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/devexp_demos/devtools_companion/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/devexp_demos/devtools_companion/macos/Runner/AppDelegate.swift b/devexp_demos/devtools_companion/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/devexp_demos/devtools_companion/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/devexp_demos/devtools_companion/macos/Runner/Base.lproj/MainMenu.xib b/devexp_demos/devtools_companion/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/devexp_demos/devtools_companion/macos/Runner/Configs/AppInfo.xcconfig b/devexp_demos/devtools_companion/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..b65f9fc --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = devtools_companion + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.devtoolsCompanion + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/devexp_demos/devtools_companion/macos/Runner/Configs/Debug.xcconfig b/devexp_demos/devtools_companion/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/devexp_demos/devtools_companion/macos/Runner/Configs/Release.xcconfig b/devexp_demos/devtools_companion/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/devexp_demos/devtools_companion/macos/Runner/Configs/Warnings.xcconfig b/devexp_demos/devtools_companion/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/devexp_demos/devtools_companion/macos/Runner/DebugProfile.entitlements b/devexp_demos/devtools_companion/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/devexp_demos/devtools_companion/macos/Runner/Info.plist b/devexp_demos/devtools_companion/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/devexp_demos/devtools_companion/macos/Runner/MainFlutterWindow.swift b/devexp_demos/devtools_companion/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/devexp_demos/devtools_companion/macos/Runner/Release.entitlements b/devexp_demos/devtools_companion/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/devexp_demos/devtools_companion/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/devexp_demos/devtools_companion/macos/RunnerTests/RunnerTests.swift b/devexp_demos/devtools_companion/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/devexp_demos/devtools_companion/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/devexp_demos/devtools_companion/pubspec.yaml b/devexp_demos/devtools_companion/pubspec.yaml new file mode 100644 index 0000000..e92a7e9 --- /dev/null +++ b/devexp_demos/devtools_companion/pubspec.yaml @@ -0,0 +1,68 @@ +name: devtools_companion +description: "Companion app for developers working on Flutter/Dart Devtools" +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.10.0 + +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + provider: ^6.1.5+1 + shadcn_ui: ^0.39.6 + flutter_markdown: ^0.7.7+1 + dio: ^5.9.0 + flutter_animate: ^4.5.2 + http: ^1.6.0 + intl: ^0.20.2 + flutter_driver: + sdk: flutter + logging: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true + + assets: + - README.md + - assets/icons/ + - assets/images/ + + fonts: + - family: Roboto + fonts: + - asset: assets/fonts/Roboto/Roboto-Thin.ttf + weight: 100 + - asset: assets/fonts/Roboto/Roboto-Light.ttf + weight: 300 + - asset: assets/fonts/Roboto/Roboto-Regular.ttf + weight: 400 + - asset: assets/fonts/Roboto/Roboto-Medium.ttf + weight: 500 + - asset: assets/fonts/Roboto/Roboto-Bold.ttf + weight: 700 + - asset: assets/fonts/Roboto/Roboto-Black.ttf + weight: 900 + - family: RobotoMono + fonts: + - asset: assets/fonts/Roboto_Mono/RobotoMono-Thin.ttf + weight: 100 + - asset: assets/fonts/Roboto_Mono/RobotoMono-Light.ttf + weight: 300 + - asset: assets/fonts/Roboto_Mono/RobotoMono-Regular.ttf + weight: 400 + - asset: assets/fonts/Roboto_Mono/RobotoMono-Medium.ttf + weight: 500 + - asset: assets/fonts/Roboto_Mono/RobotoMono-Bold.ttf + weight: 700 diff --git a/devexp_demos/devtools_companion/test/widget_test.dart b/devexp_demos/devtools_companion/test/widget_test.dart new file mode 100644 index 0000000..f970894 --- /dev/null +++ b/devexp_demos/devtools_companion/test/widget_test.dart @@ -0,0 +1,9 @@ +import 'package:devtools_companion/main.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Basic test', (WidgetTester tester) async { + await tester.pumpWidget(const DevToolsCompanionApp()); + expect(find.text('DevTools'), findsOneWidget); + }); +} diff --git a/devexp_demos/devtools_companion/web/favicon.png b/devexp_demos/devtools_companion/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/devexp_demos/devtools_companion/web/favicon.png differ diff --git a/devexp_demos/devtools_companion/web/icons/Icon-192.png b/devexp_demos/devtools_companion/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/devexp_demos/devtools_companion/web/icons/Icon-192.png differ diff --git a/devexp_demos/devtools_companion/web/icons/Icon-512.png b/devexp_demos/devtools_companion/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/devexp_demos/devtools_companion/web/icons/Icon-512.png differ diff --git a/devexp_demos/devtools_companion/web/icons/Icon-maskable-192.png b/devexp_demos/devtools_companion/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/devexp_demos/devtools_companion/web/icons/Icon-maskable-192.png differ diff --git a/devexp_demos/devtools_companion/web/icons/Icon-maskable-512.png b/devexp_demos/devtools_companion/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/devexp_demos/devtools_companion/web/icons/Icon-maskable-512.png differ diff --git a/devexp_demos/devtools_companion/web/index.html b/devexp_demos/devtools_companion/web/index.html new file mode 100644 index 0000000..be0fc21 --- /dev/null +++ b/devexp_demos/devtools_companion/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + devtools_companion + + + + + + diff --git a/devexp_demos/devtools_companion/web/manifest.json b/devexp_demos/devtools_companion/web/manifest.json new file mode 100644 index 0000000..72d0ec6 --- /dev/null +++ b/devexp_demos/devtools_companion/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "devtools_companion", + "short_name": "devtools_companion", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/vertex_ai_firebase_flutter_app/windows/.gitignore b/devexp_demos/devtools_companion/windows/.gitignore similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/.gitignore rename to devexp_demos/devtools_companion/windows/.gitignore diff --git a/vertex_ai_firebase_flutter_app/windows/CMakeLists.txt b/devexp_demos/devtools_companion/windows/CMakeLists.txt similarity index 98% rename from vertex_ai_firebase_flutter_app/windows/CMakeLists.txt rename to devexp_demos/devtools_companion/windows/CMakeLists.txt index 6e05810..1cb7a4a 100644 --- a/vertex_ai_firebase_flutter_app/windows/CMakeLists.txt +++ b/devexp_demos/devtools_companion/windows/CMakeLists.txt @@ -1,10 +1,10 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(colorist LANGUAGES CXX) +project(devtools_companion LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "colorist") +set(BINARY_NAME "devtools_companion") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/vertex_ai_firebase_flutter_app/windows/flutter/CMakeLists.txt b/devexp_demos/devtools_companion/windows/flutter/CMakeLists.txt similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/flutter/CMakeLists.txt rename to devexp_demos/devtools_companion/windows/flutter/CMakeLists.txt diff --git a/devexp_demos/devtools_companion/windows/flutter/generated_plugin_registrant.cc b/devexp_demos/devtools_companion/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/devexp_demos/devtools_companion/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.h b/devexp_demos/devtools_companion/windows/flutter/generated_plugin_registrant.h similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.h rename to devexp_demos/devtools_companion/windows/flutter/generated_plugin_registrant.h diff --git a/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugins.cmake b/devexp_demos/devtools_companion/windows/flutter/generated_plugins.cmake similarity index 95% rename from vertex_ai_firebase_flutter_app/windows/flutter/generated_plugins.cmake rename to devexp_demos/devtools_companion/windows/flutter/generated_plugins.cmake index 29944d5..b93c4c3 100644 --- a/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugins.cmake +++ b/devexp_demos/devtools_companion/windows/flutter/generated_plugins.cmake @@ -3,8 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - firebase_auth - firebase_core ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/vertex_ai_firebase_flutter_app/windows/runner/CMakeLists.txt b/devexp_demos/devtools_companion/windows/runner/CMakeLists.txt similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/CMakeLists.txt rename to devexp_demos/devtools_companion/windows/runner/CMakeLists.txt diff --git a/vertex_ai_firebase_flutter_app/windows/runner/Runner.rc b/devexp_demos/devtools_companion/windows/runner/Runner.rc similarity index 91% rename from vertex_ai_firebase_flutter_app/windows/runner/Runner.rc rename to devexp_demos/devtools_companion/windows/runner/Runner.rc index e0478fb..a992832 100644 --- a/vertex_ai_firebase_flutter_app/windows/runner/Runner.rc +++ b/devexp_demos/devtools_companion/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "colorist" "\0" + VALUE "FileDescription", "devtools_companion" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "colorist" "\0" + VALUE "InternalName", "devtools_companion" "\0" VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "colorist.exe" "\0" - VALUE "ProductName", "colorist" "\0" + VALUE "OriginalFilename", "devtools_companion.exe" "\0" + VALUE "ProductName", "devtools_companion" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/vertex_ai_firebase_flutter_app/windows/runner/flutter_window.cpp b/devexp_demos/devtools_companion/windows/runner/flutter_window.cpp similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/flutter_window.cpp rename to devexp_demos/devtools_companion/windows/runner/flutter_window.cpp diff --git a/vertex_ai_firebase_flutter_app/windows/runner/flutter_window.h b/devexp_demos/devtools_companion/windows/runner/flutter_window.h similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/flutter_window.h rename to devexp_demos/devtools_companion/windows/runner/flutter_window.h diff --git a/devexp_demos/devtools_companion/windows/runner/main.cpp b/devexp_demos/devtools_companion/windows/runner/main.cpp new file mode 100644 index 0000000..64db46d --- /dev/null +++ b/devexp_demos/devtools_companion/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"devtools_companion", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/vertex_ai_firebase_flutter_app/windows/runner/resource.h b/devexp_demos/devtools_companion/windows/runner/resource.h similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/resource.h rename to devexp_demos/devtools_companion/windows/runner/resource.h diff --git a/vertex_ai_firebase_flutter_app/windows/runner/resources/app_icon.ico b/devexp_demos/devtools_companion/windows/runner/resources/app_icon.ico similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/resources/app_icon.ico rename to devexp_demos/devtools_companion/windows/runner/resources/app_icon.ico diff --git a/vertex_ai_firebase_flutter_app/windows/runner/runner.exe.manifest b/devexp_demos/devtools_companion/windows/runner/runner.exe.manifest similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/runner.exe.manifest rename to devexp_demos/devtools_companion/windows/runner/runner.exe.manifest diff --git a/vertex_ai_firebase_flutter_app/windows/runner/utils.cpp b/devexp_demos/devtools_companion/windows/runner/utils.cpp similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/utils.cpp rename to devexp_demos/devtools_companion/windows/runner/utils.cpp diff --git a/vertex_ai_firebase_flutter_app/windows/runner/utils.h b/devexp_demos/devtools_companion/windows/runner/utils.h similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/utils.h rename to devexp_demos/devtools_companion/windows/runner/utils.h diff --git a/vertex_ai_firebase_flutter_app/windows/runner/win32_window.cpp b/devexp_demos/devtools_companion/windows/runner/win32_window.cpp similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/win32_window.cpp rename to devexp_demos/devtools_companion/windows/runner/win32_window.cpp diff --git a/vertex_ai_firebase_flutter_app/windows/runner/win32_window.h b/devexp_demos/devtools_companion/windows/runner/win32_window.h similarity index 100% rename from vertex_ai_firebase_flutter_app/windows/runner/win32_window.h rename to devexp_demos/devtools_companion/windows/runner/win32_window.h diff --git a/federated_plugin/README.md b/federated_plugin/README.md new file mode 100644 index 0000000..2dc52b3 --- /dev/null +++ b/federated_plugin/README.md @@ -0,0 +1,20 @@ +# federated_plugin + +A Flutter plugin sample that shows how to implement a federated plugin to retrieve current battery level on different platforms. + +This sample is currently being built. Not all platforms and functionality are in place. + +## Goals for this sample + +* Show how to develop a federated plugin which supports Android, iOS, web & desktop. +* Demonstrate how to use Platform Channels to communicate with different platforms including Web and Desktop. + +## Questions/issues + +If you have a general question about Flutter, the best places to go are: + +* [The FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev) +* [The Flutter Gitter channel](https://gitter.im/flutter/flutter) +* [StackOverflow](https://stackoverflow.com/questions/tagged/flutter) + +If you run into an issue with the sample itself, please file an issue [here](https://github.com/flutter/samples/issues). \ No newline at end of file diff --git a/federated_plugin/federated_plugin/.gitignore b/federated_plugin/federated_plugin/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/federated_plugin/federated_plugin/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/federated_plugin/federated_plugin/.metadata b/federated_plugin/federated_plugin/.metadata new file mode 100644 index 0000000..8c15ad7 --- /dev/null +++ b/federated_plugin/federated_plugin/.metadata @@ -0,0 +1,10 @@ +# 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: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: plugin diff --git a/federated_plugin/federated_plugin/CHANGELOG.md b/federated_plugin/federated_plugin/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/federated_plugin/federated_plugin/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/federated_plugin/federated_plugin/analysis_options.yaml b/federated_plugin/federated_plugin/analysis_options.yaml new file mode 100644 index 0000000..13d6fe1 --- /dev/null +++ b/federated_plugin/federated_plugin/analysis_options.yaml @@ -0,0 +1 @@ +include: package:analysis_defaults/flutter.yaml diff --git a/federated_plugin/federated_plugin/android/.gitignore b/federated_plugin/federated_plugin/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/federated_plugin/federated_plugin/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/federated_plugin/federated_plugin/android/build.gradle b/federated_plugin/federated_plugin/android/build.gradle new file mode 100644 index 0000000..4e6d924 --- /dev/null +++ b/federated_plugin/federated_plugin/android/build.gradle @@ -0,0 +1,50 @@ +group 'dev.flutter.federated_plugin' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 30 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/federated_plugin/federated_plugin/android/settings.gradle b/federated_plugin/federated_plugin/android/settings.gradle new file mode 100644 index 0000000..8f719cd --- /dev/null +++ b/federated_plugin/federated_plugin/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'federated_plugin' diff --git a/federated_plugin/federated_plugin/android/src/main/AndroidManifest.xml b/federated_plugin/federated_plugin/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..22a910c --- /dev/null +++ b/federated_plugin/federated_plugin/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/federated_plugin/federated_plugin/android/src/main/kotlin/dev/flutter/federated_plugin/FederatedPlugin.kt b/federated_plugin/federated_plugin/android/src/main/kotlin/dev/flutter/federated_plugin/FederatedPlugin.kt new file mode 100644 index 0000000..c85fecf --- /dev/null +++ b/federated_plugin/federated_plugin/android/src/main/kotlin/dev/flutter/federated_plugin/FederatedPlugin.kt @@ -0,0 +1,58 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.federated_plugin + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.Build +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +class FederatedPlugin : FlutterPlugin, MethodCallHandler { + private lateinit var channel: MethodChannel + private var context: Context? = null + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "battery") + channel.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getBatteryLevel") { + val batteryLevel: Int + + batteryLevel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val batteryManager = context?.getSystemService(Context.BATTERY_SERVICE) as BatteryManager + batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + } else { + val intent = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { intentFilter -> + context?.registerReceiver(null, intentFilter) + } + intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + } + + if (batteryLevel < 0) { + result.error("STATUS_UNAVAILABLE", "Not able to determine battery level.", null) + } else { + result.success(batteryLevel) + } + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + context = null + } +} diff --git a/federated_plugin/federated_plugin/example/.gitignore b/federated_plugin/federated_plugin/example/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/federated_plugin/federated_plugin/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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 +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# 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 diff --git a/federated_plugin/federated_plugin/example/.metadata b/federated_plugin/federated_plugin/example/.metadata new file mode 100644 index 0000000..fd70cab --- /dev/null +++ b/federated_plugin/federated_plugin/example/.metadata @@ -0,0 +1,10 @@ +# 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: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: app diff --git a/federated_plugin/federated_plugin/example/README.md b/federated_plugin/federated_plugin/example/README.md new file mode 100644 index 0000000..6cd01f2 --- /dev/null +++ b/federated_plugin/federated_plugin/example/README.md @@ -0,0 +1,3 @@ +# federated_plugin_example + +Demonstrates how to use the federated_plugin plugin. diff --git a/federated_plugin/federated_plugin/example/analysis_options.yaml b/federated_plugin/federated_plugin/example/analysis_options.yaml new file mode 100644 index 0000000..13d6fe1 --- /dev/null +++ b/federated_plugin/federated_plugin/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:analysis_defaults/flutter.yaml diff --git a/federated_plugin/federated_plugin/example/android/.gitignore b/federated_plugin/federated_plugin/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/federated_plugin/federated_plugin/example/android/app/build.gradle b/federated_plugin/federated_plugin/example/android/app/build.gradle new file mode 100644 index 0000000..48b36ec --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/build.gradle @@ -0,0 +1,68 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "dev.flutter.federated_plugin_example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/federated_plugin/federated_plugin/example/android/app/src/debug/AndroidManifest.xml b/federated_plugin/federated_plugin/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..7d52523 --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/AndroidManifest.xml b/federated_plugin/federated_plugin/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..887b665 --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/kotlin/dev/flutter/example/MainActivity.kt b/federated_plugin/federated_plugin/example/android/app/src/main/kotlin/dev/flutter/example/MainActivity.kt new file mode 100644 index 0000000..d37e09f --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/src/main/kotlin/dev/flutter/example/MainActivity.kt @@ -0,0 +1,6 @@ +package dev.flutter.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/kotlin/dev/flutter/federated_plugin_example/MainActivity.kt b/federated_plugin/federated_plugin/example/android/app/src/main/kotlin/dev/flutter/federated_plugin_example/MainActivity.kt new file mode 100644 index 0000000..de6cd71 --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/src/main/kotlin/dev/flutter/federated_plugin_example/MainActivity.kt @@ -0,0 +1,6 @@ +package dev.flutter.federated_plugin_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/res/drawable-v21/launch_background.xml b/federated_plugin/federated_plugin/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/res/drawable/launch_background.xml b/federated_plugin/federated_plugin/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/federated_plugin/federated_plugin/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/res/values-night/styles.xml b/federated_plugin/federated_plugin/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..3db14bb --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/federated_plugin/federated_plugin/example/android/app/src/main/res/values/styles.xml b/federated_plugin/federated_plugin/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d460d1e --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/federated_plugin/federated_plugin/example/android/app/src/profile/AndroidManifest.xml b/federated_plugin/federated_plugin/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..7d52523 --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/federated_plugin/federated_plugin/example/android/build.gradle b/federated_plugin/federated_plugin/example/android/build.gradle new file mode 100644 index 0000000..24047dc --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/federated_plugin/federated_plugin/example/android/gradle.properties b/federated_plugin/federated_plugin/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/federated_plugin/federated_plugin/example/android/gradle/wrapper/gradle-wrapper.properties b/federated_plugin/federated_plugin/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bc6a58a --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/federated_plugin/federated_plugin/example/android/settings.gradle b/federated_plugin/federated_plugin/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/federated_plugin/federated_plugin/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/federated_plugin/federated_plugin/example/ios/.gitignore b/federated_plugin/federated_plugin/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +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 diff --git a/federated_plugin/federated_plugin/example/ios/Flutter/AppFrameworkInfo.plist b/federated_plugin/federated_plugin/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/federated_plugin/federated_plugin/example/ios/Flutter/Debug.xcconfig b/federated_plugin/federated_plugin/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/federated_plugin/federated_plugin/example/ios/Flutter/Release.xcconfig b/federated_plugin/federated_plugin/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/federated_plugin/federated_plugin/example/ios/Podfile b/federated_plugin/federated_plugin/example/ios/Podfile new file mode 100644 index 0000000..1e8c3c9 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.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! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.pbxproj b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8bae941 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,549 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 05F5AA9A8918E7054FF69BBB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30D3D9621395C516E82FBA97 /* Pods_Runner.framework */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile 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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 30D3D9621395C516E82FBA97 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 447027611B96046137396C04 /* 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 = ""; }; + 66FD4F9678EE12C6021DDC4C /* 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AE02D38AA281AD19B3FBAE7B /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05F5AA9A8918E7054FF69BBB /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0F9960076300DD52FCCDCAC1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 30D3D9621395C516E82FBA97 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 44C3AAFD416E2CBFA5D43075 /* Pods */ = { + isa = PBXGroup; + children = ( + 447027611B96046137396C04 /* Pods-Runner.debug.xcconfig */, + 66FD4F9678EE12C6021DDC4C /* Pods-Runner.release.xcconfig */, + AE02D38AA281AD19B3FBAE7B /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 44C3AAFD416E2CBFA5D43075 /* Pods */, + 0F9960076300DD52FCCDCAC1 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C01D951D1D8DEFCA6C06A108 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + F9CBF189CFE3019A377CBAB3 /* [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 = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 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 */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 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 */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + 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; + 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"; + }; + C01D951D1D8DEFCA6C06A108 /* [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; + }; + F9CBF189CFE3019A377CBAB3 /* [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 */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + 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 = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.federatedPluginExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + 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 = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + 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 = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.federatedPluginExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.federatedPluginExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */ + 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 */; +} diff --git a/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/federated_plugin/federated_plugin/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/federated_plugin/federated_plugin/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/federated_plugin/federated_plugin/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner/AppDelegate.swift b/federated_plugin/federated_plugin/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/federated_plugin/federated_plugin/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Base.lproj/Main.storyboard b/federated_plugin/federated_plugin/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Info.plist b/federated_plugin/federated_plugin/example/ios/Runner/Info.plist new file mode 100644 index 0000000..ca05d0b --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Federated Plugin + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + federated_plugin_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/federated_plugin/federated_plugin/example/ios/Runner/Runner-Bridging-Header.h b/federated_plugin/federated_plugin/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/federated_plugin/federated_plugin/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/federated_plugin/federated_plugin/example/lib/main.dart b/federated_plugin/federated_plugin/example/lib/main.dart new file mode 100644 index 0000000..c17ad14 --- /dev/null +++ b/federated_plugin/federated_plugin/example/lib/main.dart @@ -0,0 +1,72 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:federated_plugin/federated_plugin.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp(theme: ThemeData.light(), home: const HomePage()); + } +} + +/// Demonstrates how to use the getBatteryLevel method from federated_plugin to retrieve +/// current battery level of device. +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + int? batteryLevel; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Federated Plugin Demo')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + batteryLevel == null + ? const SizedBox.shrink() + : Text( + 'Battery Level: $batteryLevel', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + FilledButton( + onPressed: () async { + try { + final result = await getBatteryLevel(); + setState(() { + batteryLevel = result; + }); + } catch (error) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Theme.of(context).primaryColor, + content: Text((error as dynamic).message as String), + ), + ); + } + }, + child: const Text('Get Battery Level'), + ), + ], + ), + ), + ); + } +} diff --git a/federated_plugin/federated_plugin/example/macos/.gitignore b/federated_plugin/federated_plugin/example/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/federated_plugin/federated_plugin/example/macos/Flutter/Flutter-Debug.xcconfig b/federated_plugin/federated_plugin/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/federated_plugin/federated_plugin/example/macos/Flutter/Flutter-Release.xcconfig b/federated_plugin/federated_plugin/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/federated_plugin/federated_plugin/example/macos/Flutter/GeneratedPluginRegistrant.swift b/federated_plugin/federated_plugin/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..0d51469 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import federated_plugin_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FederatedPluginMacosPlugin.register(with: registry.registrar(forPlugin: "FederatedPluginMacosPlugin")) +} diff --git a/federated_plugin/federated_plugin/example/macos/Podfile b/federated_plugin/federated_plugin/example/macos/Podfile new file mode 100644 index 0000000..dade8df --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/federated_plugin/federated_plugin/example/macos/Runner.xcodeproj/project.pbxproj b/federated_plugin/federated_plugin/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..659aec3 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + C58C13458B8C1F1C0B003A42 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B52B3AE71320C78F7034ECA8 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1671D9D3B0356EFB54861951 /* 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 = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 45FDBBDD4581EECA19893003 /* 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 = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 8FC351F79983970A66307AC9 /* 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 = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + B52B3AE71320C78F7034ECA8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C58C13458B8C1F1C0B003A42 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2B77B711C096CBDA06689A67 /* Pods */ = { + isa = PBXGroup; + children = ( + 45FDBBDD4581EECA19893003 /* Pods-Runner.debug.xcconfig */, + 8FC351F79983970A66307AC9 /* Pods-Runner.release.xcconfig */, + 1671D9D3B0356EFB54861951 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 2B77B711C096CBDA06689A67 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B52B3AE71320C78F7034ECA8 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C6FC087C0C366A8D70EA0FE7 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 50A44A481605D096E0AB4045 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 50A44A481605D096E0AB4045 /* [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; + }; + C6FC087C0C366A8D70EA0FE7 /* [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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/federated_plugin/federated_plugin/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/federated_plugin/federated_plugin/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/federated_plugin/federated_plugin/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/federated_plugin/federated_plugin/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..fb7259e --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/federated_plugin/federated_plugin/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/federated_plugin/federated_plugin/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/federated_plugin/federated_plugin/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/federated_plugin/federated_plugin/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/federated_plugin/federated_plugin/example/macos/Runner/AppDelegate.swift b/federated_plugin/federated_plugin/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..3c4935a Binary files /dev/null and b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..ed4cc16 Binary files /dev/null and b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..483be61 Binary files /dev/null and b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bcbf36d Binary files /dev/null and b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..9c0a652 Binary files /dev/null and b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..e71a726 Binary files /dev/null and b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..8a31fe2 Binary files /dev/null and b/federated_plugin/federated_plugin/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Base.lproj/MainMenu.xib b/federated_plugin/federated_plugin/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..537341a --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Configs/AppInfo.xcconfig b/federated_plugin/federated_plugin/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..a1f7408 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 dev.flutter. All rights reserved. diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Configs/Debug.xcconfig b/federated_plugin/federated_plugin/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Configs/Release.xcconfig b/federated_plugin/federated_plugin/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Configs/Warnings.xcconfig b/federated_plugin/federated_plugin/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/federated_plugin/federated_plugin/example/macos/Runner/DebugProfile.entitlements b/federated_plugin/federated_plugin/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Info.plist b/federated_plugin/federated_plugin/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/federated_plugin/federated_plugin/example/macos/Runner/MainFlutterWindow.swift b/federated_plugin/federated_plugin/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/federated_plugin/federated_plugin/example/macos/Runner/Release.entitlements b/federated_plugin/federated_plugin/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/federated_plugin/federated_plugin/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/federated_plugin/federated_plugin/example/pubspec.yaml b/federated_plugin/federated_plugin/example/pubspec.yaml new file mode 100644 index 0000000..03bea4d --- /dev/null +++ b/federated_plugin/federated_plugin/example/pubspec.yaml @@ -0,0 +1,24 @@ +name: federated_plugin_example +description: Demonstrates how to use the federated_plugin plugin. + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ^3.7.0-0 + +dependencies: + flutter: + sdk: flutter + + federated_plugin: + path: ../ + cupertino_icons: ^1.0.2 + +dev_dependencies: + analysis_defaults: + path: ../../../../analysis_defaults + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/federated_plugin/federated_plugin/example/test/widget_test.dart b/federated_plugin/federated_plugin/example/test/widget_test.dart new file mode 100644 index 0000000..597a07b --- /dev/null +++ b/federated_plugin/federated_plugin/example/test/widget_test.dart @@ -0,0 +1,33 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:federated_plugin_example/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('federated plugin demo tests', () { + const batteryLevel = 45; + + testWidgets('get current battery level from platform', (tester) async { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('battery'), + (call) async { + if (call.method == 'getBatteryLevel') { + return batteryLevel; + } + return 0; + }, + ); + await tester.pumpWidget(const MyApp()); + + // Tap button to retrieve current battery level from platform. + await tester.tap(find.byType(FilledButton)); + await tester.pumpAndSettle(); + + expect(find.text('Battery Level: $batteryLevel'), findsOneWidget); + }); + }); +} diff --git a/federated_plugin/federated_plugin/example/web/favicon.png b/federated_plugin/federated_plugin/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/federated_plugin/federated_plugin/example/web/favicon.png differ diff --git a/federated_plugin/federated_plugin/example/web/icons/Icon-192.png b/federated_plugin/federated_plugin/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/federated_plugin/federated_plugin/example/web/icons/Icon-192.png differ diff --git a/federated_plugin/federated_plugin/example/web/icons/Icon-512.png b/federated_plugin/federated_plugin/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/federated_plugin/federated_plugin/example/web/icons/Icon-512.png differ diff --git a/federated_plugin/federated_plugin/example/web/icons/Icon-maskable-192.png b/federated_plugin/federated_plugin/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/federated_plugin/federated_plugin/example/web/icons/Icon-maskable-192.png differ diff --git a/federated_plugin/federated_plugin/example/web/icons/Icon-maskable-512.png b/federated_plugin/federated_plugin/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/federated_plugin/federated_plugin/example/web/icons/Icon-maskable-512.png differ diff --git a/federated_plugin/federated_plugin/example/web/index.html b/federated_plugin/federated_plugin/example/web/index.html new file mode 100644 index 0000000..b6b9dd2 --- /dev/null +++ b/federated_plugin/federated_plugin/example/web/index.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + diff --git a/federated_plugin/federated_plugin/example/web/manifest.json b/federated_plugin/federated_plugin/example/web/manifest.json new file mode 100644 index 0000000..096edf8 --- /dev/null +++ b/federated_plugin/federated_plugin/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/federated_plugin/federated_plugin/example/windows/.gitignore b/federated_plugin/federated_plugin/example/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/federated_plugin/federated_plugin/example/windows/CMakeLists.txt b/federated_plugin/federated_plugin/example/windows/CMakeLists.txt new file mode 100644 index 0000000..1633297 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/federated_plugin/federated_plugin/example/windows/flutter/CMakeLists.txt b/federated_plugin/federated_plugin/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..b2e4bd8 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/federated_plugin/federated_plugin/example/windows/flutter/generated_plugin_registrant.cc b/federated_plugin/federated_plugin/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..369d3ba --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FederatedPluginWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FederatedPluginWindowsPlugin")); +} diff --git a/federated_plugin/federated_plugin/example/windows/flutter/generated_plugin_registrant.h b/federated_plugin/federated_plugin/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/federated_plugin/federated_plugin/example/windows/flutter/generated_plugins.cmake b/federated_plugin/federated_plugin/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..d9e53e8 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + federated_plugin_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/federated_plugin/federated_plugin/example/windows/runner/CMakeLists.txt b/federated_plugin/federated_plugin/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..de2d891 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/federated_plugin/federated_plugin/example/windows/runner/Runner.rc b/federated_plugin/federated_plugin/example/windows/runner/Runner.rc new file mode 100644 index 0000000..13cc8e8 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "dev.flutter" "\0" + VALUE "FileDescription", "A new Flutter project." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 dev.flutter. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/federated_plugin/federated_plugin/example/windows/runner/flutter_window.cpp b/federated_plugin/federated_plugin/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..b43b909 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/federated_plugin/federated_plugin/example/windows/runner/flutter_window.h b/federated_plugin/federated_plugin/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/vertex_ai_firebase_flutter_app/windows/runner/main.cpp b/federated_plugin/federated_plugin/example/windows/runner/main.cpp similarity index 95% rename from vertex_ai_firebase_flutter_app/windows/runner/main.cpp rename to federated_plugin/federated_plugin/example/windows/runner/main.cpp index 021ad6b..bcb57b0 100644 --- a/vertex_ai_firebase_flutter_app/windows/runner/main.cpp +++ b/federated_plugin/federated_plugin/example/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"colorist", origin, size)) { + if (!window.CreateAndShow(L"example", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/federated_plugin/federated_plugin/example/windows/runner/resource.h b/federated_plugin/federated_plugin/example/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/federated_plugin/federated_plugin/example/windows/runner/resources/app_icon.ico b/federated_plugin/federated_plugin/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/federated_plugin/federated_plugin/example/windows/runner/resources/app_icon.ico differ diff --git a/federated_plugin/federated_plugin/example/windows/runner/runner.exe.manifest b/federated_plugin/federated_plugin/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..c977c4a --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/federated_plugin/federated_plugin/example/windows/runner/utils.cpp b/federated_plugin/federated_plugin/example/windows/runner/utils.cpp new file mode 100644 index 0000000..d19bdbb --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/federated_plugin/federated_plugin/example/windows/runner/utils.h b/federated_plugin/federated_plugin/example/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/federated_plugin/federated_plugin/example/windows/runner/win32_window.cpp b/federated_plugin/federated_plugin/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..c10f08d --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/federated_plugin/federated_plugin/example/windows/runner/win32_window.h b/federated_plugin/federated_plugin/example/windows/runner/win32_window.h new file mode 100644 index 0000000..17ba431 --- /dev/null +++ b/federated_plugin/federated_plugin/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/federated_plugin/federated_plugin/ios/.gitignore b/federated_plugin/federated_plugin/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/federated_plugin/federated_plugin/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/federated_plugin/federated_plugin/ios/Assets/.gitkeep b/federated_plugin/federated_plugin/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/federated_plugin/federated_plugin/ios/Classes/FederatedPlugin.h b/federated_plugin/federated_plugin/ios/Classes/FederatedPlugin.h new file mode 100644 index 0000000..ebcdf4e --- /dev/null +++ b/federated_plugin/federated_plugin/ios/Classes/FederatedPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface FederatedPlugin : NSObject +@end diff --git a/federated_plugin/federated_plugin/ios/Classes/FederatedPlugin.m b/federated_plugin/federated_plugin/ios/Classes/FederatedPlugin.m new file mode 100644 index 0000000..bedb63c --- /dev/null +++ b/federated_plugin/federated_plugin/ios/Classes/FederatedPlugin.m @@ -0,0 +1,15 @@ +#import "FederatedPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "federated_plugin-Swift.h" +#endif + +@implementation FederatedPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftFederatedPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/federated_plugin/federated_plugin/ios/Classes/SwiftFederatedPlugin.swift b/federated_plugin/federated_plugin/ios/Classes/SwiftFederatedPlugin.swift new file mode 100644 index 0000000..689bb2f --- /dev/null +++ b/federated_plugin/federated_plugin/ios/Classes/SwiftFederatedPlugin.swift @@ -0,0 +1,30 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter +import UIKit + +public class SwiftFederatedPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "battery", binaryMessenger: registrar.messenger()) + let instance = SwiftFederatedPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard call.method == "getBatteryLevel" else { + result(FlutterMethodNotImplemented) + return + } + + let device = UIDevice.current + device.isBatteryMonitoringEnabled = true + + if device.batteryState == UIDevice.BatteryState.unknown { + result(FlutterError(code: "STATUS_UNAVAILABLE", message: "Not able to determine battery level", details: nil)) + } + + result(Int(device.batteryLevel * 100)) + } +} diff --git a/federated_plugin/federated_plugin/ios/federated_plugin.podspec b/federated_plugin/federated_plugin/ios/federated_plugin.podspec new file mode 100644 index 0000000..2a2fede --- /dev/null +++ b/federated_plugin/federated_plugin/ios/federated_plugin.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint federated_plugin.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'federated_plugin' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/federated_plugin/federated_plugin/lib/federated_plugin.dart b/federated_plugin/federated_plugin/lib/federated_plugin.dart new file mode 100644 index 0000000..6b83ab7 --- /dev/null +++ b/federated_plugin/federated_plugin/lib/federated_plugin.dart @@ -0,0 +1,14 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:federated_plugin_platform_interface/federated_plugin_platform_interface.dart'; + +/// Returns the current battery level of device. +/// +/// It uses [FederatedPluginInterface] interface to provide current battery level. +Future getBatteryLevel() async { + return await FederatedPluginInterface.instance.getBatteryLevel(); +} diff --git a/federated_plugin/federated_plugin/pubspec.yaml b/federated_plugin/federated_plugin/pubspec.yaml new file mode 100644 index 0000000..b268542 --- /dev/null +++ b/federated_plugin/federated_plugin/pubspec.yaml @@ -0,0 +1,41 @@ +name: federated_plugin +description: A new flutter plugin project to demonstrate how to implement federated plugin. +version: 0.0.1 + +publish_to: "none" + +environment: + sdk: ^3.7.0-0 + +dependencies: + flutter: + sdk: flutter + federated_plugin_platform_interface: + path: ../federated_plugin_platform_interface + federated_plugin_web: + path: ../federated_plugin_web + federated_plugin_windows: + path: ../federated_plugin_windows + federated_plugin_macos: + path: ../federated_plugin_macos + +dev_dependencies: + analysis_defaults: + path: ../../../analysis_defaults + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + package: dev.flutter.federated_plugin + pluginClass: FederatedPlugin + ios: + pluginClass: SwiftFederatedPlugin + web: + default_package: federated_plugin_web + windows: + default_package: federated_plugin_windows + macos: + default_package: federated_plugin_macos diff --git a/federated_plugin/federated_plugin/test/federated_plugin_test.dart b/federated_plugin/federated_plugin/test/federated_plugin_test.dart new file mode 100644 index 0000000..833fda4 --- /dev/null +++ b/federated_plugin/federated_plugin/test/federated_plugin_test.dart @@ -0,0 +1,29 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:federated_plugin/federated_plugin.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Federated Plugin Test', () { + const batteryLevel = 34; + + testWidgets('getBatteryLevel method test', (tester) async { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('battery'), + (call) async { + if (call.method == 'getBatteryLevel') { + return batteryLevel; + } + return 0; + }, + ); + final result = await getBatteryLevel(); + expect(result, batteryLevel); + }); + }); +} diff --git a/federated_plugin/federated_plugin_macos/.gitignore b/federated_plugin/federated_plugin_macos/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/federated_plugin/federated_plugin_macos/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/federated_plugin/federated_plugin_macos/.metadata b/federated_plugin/federated_plugin_macos/.metadata new file mode 100644 index 0000000..8c15ad7 --- /dev/null +++ b/federated_plugin/federated_plugin_macos/.metadata @@ -0,0 +1,10 @@ +# 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: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: plugin diff --git a/federated_plugin/federated_plugin_macos/analysis_options.yaml b/federated_plugin/federated_plugin_macos/analysis_options.yaml new file mode 100644 index 0000000..13d6fe1 --- /dev/null +++ b/federated_plugin/federated_plugin_macos/analysis_options.yaml @@ -0,0 +1 @@ +include: package:analysis_defaults/flutter.yaml diff --git a/federated_plugin/federated_plugin_macos/example/README.md b/federated_plugin/federated_plugin_macos/example/README.md new file mode 100644 index 0000000..922ad9c --- /dev/null +++ b/federated_plugin/federated_plugin_macos/example/README.md @@ -0,0 +1,3 @@ +# federated_plugin_macos_example + +To view the usage of plugin, head over to [federated_plugin/example](../../federated_plugin/example). diff --git a/federated_plugin/federated_plugin_macos/lib/federated_plugin_macos.dart b/federated_plugin/federated_plugin_macos/lib/federated_plugin_macos.dart new file mode 100644 index 0000000..76d0f54 --- /dev/null +++ b/federated_plugin/federated_plugin_macos/lib/federated_plugin_macos.dart @@ -0,0 +1,2 @@ +// The federated_plugin_macos uses the default BatteryMethodChannel used by +// federated_plugin_platform_interface to do platform calls. diff --git a/federated_plugin/federated_plugin_macos/macos/Classes/FederatedPluginMacosPlugin.swift b/federated_plugin/federated_plugin_macos/macos/Classes/FederatedPluginMacosPlugin.swift new file mode 100644 index 0000000..8375b0e --- /dev/null +++ b/federated_plugin/federated_plugin_macos/macos/Classes/FederatedPluginMacosPlugin.swift @@ -0,0 +1,37 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS +import IOKit.ps + +public class FederatedPluginMacosPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "battery", binaryMessenger: registrar.messenger) + let instance = FederatedPluginMacosPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getBatteryLevel": + getBatteryLevel(result) + default: + result(FlutterMethodNotImplemented) + } + } + + private func getBatteryLevel(_ result: FlutterResult) { + let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue() + let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array + let sourceInfo : NSDictionary = IOPSGetPowerSourceDescription(snapshot, sources[0]).takeUnretainedValue() + + guard let capacity = sourceInfo[kIOPSCurrentCapacityKey] as? Int else { + result(FlutterError(code: "STATUS_UNAVAILABLE", message: "Not able to determine battery level", details: nil)) + return + } + + result(capacity) + } +} diff --git a/federated_plugin/federated_plugin_macos/macos/federated_plugin_macos.podspec b/federated_plugin/federated_plugin_macos/macos/federated_plugin_macos.podspec new file mode 100644 index 0000000..b918b59 --- /dev/null +++ b/federated_plugin/federated_plugin_macos/macos/federated_plugin_macos.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint federated_plugin_macos.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'federated_plugin_macos' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/federated_plugin/federated_plugin_macos/pubspec.yaml b/federated_plugin/federated_plugin_macos/pubspec.yaml new file mode 100644 index 0000000..8eaef2b --- /dev/null +++ b/federated_plugin/federated_plugin_macos/pubspec.yaml @@ -0,0 +1,23 @@ +name: federated_plugin_macos +description: macOS implementation of federated_plugin to retrieve current battery level. +version: 0.0.1 +homepage: + +environment: + sdk: ^3.7.0-0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + analysis_defaults: + path: ../../../analysis_defaults + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + macos: + pluginClass: FederatedPluginMacosPlugin diff --git a/federated_plugin/federated_plugin_platform_interface/.gitignore b/federated_plugin/federated_plugin_platform_interface/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/federated_plugin/federated_plugin_platform_interface/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/federated_plugin/federated_plugin_platform_interface/.metadata b/federated_plugin/federated_plugin_platform_interface/.metadata new file mode 100644 index 0000000..af84dae --- /dev/null +++ b/federated_plugin/federated_plugin_platform_interface/.metadata @@ -0,0 +1,10 @@ +# 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: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: package diff --git a/federated_plugin/federated_plugin_platform_interface/analysis_options.yaml b/federated_plugin/federated_plugin_platform_interface/analysis_options.yaml new file mode 100644 index 0000000..13d6fe1 --- /dev/null +++ b/federated_plugin/federated_plugin_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: package:analysis_defaults/flutter.yaml diff --git a/federated_plugin/federated_plugin_platform_interface/lib/battery_method_channel.dart b/federated_plugin/federated_plugin_platform_interface/lib/battery_method_channel.dart new file mode 100644 index 0000000..11f31ad --- /dev/null +++ b/federated_plugin/federated_plugin_platform_interface/lib/battery_method_channel.dart @@ -0,0 +1,17 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:federated_plugin_platform_interface/federated_plugin_platform_interface.dart'; +import 'package:flutter/services.dart'; + +/// Implements [FederatedPluginInterface] using [MethodChannel] to fetch +/// battery level from platform. +class BatteryMethodChannel extends FederatedPluginInterface { + static const MethodChannel _methodChannel = MethodChannel('battery'); + + @override + Future getBatteryLevel() async { + return await _methodChannel.invokeMethod('getBatteryLevel') as int; + } +} diff --git a/federated_plugin/federated_plugin_platform_interface/lib/federated_plugin_platform_interface.dart b/federated_plugin/federated_plugin_platform_interface/lib/federated_plugin_platform_interface.dart new file mode 100644 index 0000000..595c82f --- /dev/null +++ b/federated_plugin/federated_plugin_platform_interface/lib/federated_plugin_platform_interface.dart @@ -0,0 +1,30 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:federated_plugin_platform_interface/battery_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// Interface which allows all the platform plugins to implement the same +/// functionality. +abstract class FederatedPluginInterface extends PlatformInterface { + FederatedPluginInterface() : super(token: _object); + + static FederatedPluginInterface _federatedPluginInterface = + BatteryMethodChannel(); + + static final Object _object = Object(); + + /// Provides instance of [BatteryMethodChannel] to invoke platform calls. + static FederatedPluginInterface get instance => _federatedPluginInterface; + + static set instance(FederatedPluginInterface instance) { + PlatformInterface.verifyToken(instance, _object); + _federatedPluginInterface = instance; + } + + /// Returns the current battery level of device. + Future getBatteryLevel() async { + throw UnimplementedError('getBatteryLevel() has not been implemented.'); + } +} diff --git a/federated_plugin/federated_plugin_platform_interface/pubspec.yaml b/federated_plugin/federated_plugin_platform_interface/pubspec.yaml new file mode 100644 index 0000000..11bbb86 --- /dev/null +++ b/federated_plugin/federated_plugin_platform_interface/pubspec.yaml @@ -0,0 +1,18 @@ +name: federated_plugin_platform_interface +description: A platform interface for federated_plugin. +version: 0.0.1 +homepage: + +environment: + sdk: ^3.7.0-0 + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + analysis_defaults: + path: ../../../analysis_defaults + flutter_test: + sdk: flutter diff --git a/federated_plugin/federated_plugin_platform_interface/test/federated_plugin_platform_interface_test.dart b/federated_plugin/federated_plugin_platform_interface/test/federated_plugin_platform_interface_test.dart new file mode 100644 index 0000000..3c302ab --- /dev/null +++ b/federated_plugin/federated_plugin_platform_interface/test/federated_plugin_platform_interface_test.dart @@ -0,0 +1,30 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:federated_plugin_platform_interface/battery_method_channel.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('MethodChannel test', () { + const batteryLevel = 89; + + testWidgets('getBatteryLevel method test', (tester) async { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('battery'), + (call) async { + if (call.method == 'getBatteryLevel') { + return batteryLevel; + } + return 0; + }, + ); + final locationMethodChannel = BatteryMethodChannel(); + final result = await locationMethodChannel.getBatteryLevel(); + expect(result, batteryLevel); + }); + }); +} diff --git a/federated_plugin/federated_plugin_web/.gitignore b/federated_plugin/federated_plugin_web/.gitignore new file mode 100644 index 0000000..89aaf2a --- /dev/null +++ b/federated_plugin/federated_plugin_web/.gitignore @@ -0,0 +1,78 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Web related +lib/generated_plugin_registrant.dart + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/federated_plugin/federated_plugin_web/.metadata b/federated_plugin/federated_plugin_web/.metadata new file mode 100644 index 0000000..8d52aa4 --- /dev/null +++ b/federated_plugin/federated_plugin_web/.metadata @@ -0,0 +1,10 @@ +# 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: 8c5c720ce60b1761ec2963053e0d415df60a29e1 + channel: master + +project_type: package diff --git a/federated_plugin/federated_plugin_web/README.md b/federated_plugin/federated_plugin_web/README.md new file mode 100644 index 0000000..8ad8ce4 --- /dev/null +++ b/federated_plugin/federated_plugin_web/README.md @@ -0,0 +1,11 @@ +# federated_plugin_web + +A flutter plugin to provide location support for web. The web implementation +of `federated_plugin` is tested using [integration_test](https://pub.dev/packages/integration_test) package. + +### Steps to run integration test on browser + +- Download and install the ChromeDriver from [here](https://chromedriver.chromium.org/downloads) +for the version of Chrome you are using. +- Start the driver using `chromedrive --port=4444` +- Run the test using `flutter drive -d web-server --browser-name=chrome --release --target=test_driver/federated_plugin_web_integration.dart` diff --git a/federated_plugin/federated_plugin_web/analysis_options.yaml b/federated_plugin/federated_plugin_web/analysis_options.yaml new file mode 100644 index 0000000..13d6fe1 --- /dev/null +++ b/federated_plugin/federated_plugin_web/analysis_options.yaml @@ -0,0 +1 @@ +include: package:analysis_defaults/flutter.yaml diff --git a/federated_plugin/federated_plugin_web/lib/federated_plugin_web.dart b/federated_plugin/federated_plugin_web/lib/federated_plugin_web.dart new file mode 100644 index 0000000..da628a4 --- /dev/null +++ b/federated_plugin/federated_plugin_web/lib/federated_plugin_web.dart @@ -0,0 +1,43 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:federated_plugin_platform_interface/federated_plugin_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:web/web.dart' as html; + +/// Web Implementation of [FederatedPluginInterface] to retrieve current battery +/// level of device. +class FederatedPlugin extends FederatedPluginInterface { + final html.Navigator _navigator; + + /// Constructor to override the navigator object for testing purpose. + FederatedPlugin({html.Navigator? navigator}) + : _navigator = navigator ?? html.window.navigator; + + /// Method to register the plugin which sets [FederatedPlugin] to be the default + /// instance of [FederatedPluginInterface]. + static void registerWith(Registrar registrar) { + FederatedPluginInterface.instance = FederatedPlugin(); + } + + /// Returns the current battery level of device. + /// + /// If any error, it's assume that the BatteryManager API is not supported by + /// browser. + @override + Future getBatteryLevel() async { + try { + final battery = _navigator.getBattery() as html.BatteryManager; + // The battery level retrieved is in range of 0.0 to 1.0. + return battery.level * 100 as int; + } catch (error) { + throw PlatformException( + code: 'STATUS_UNAVAILABLE', + message: 'The plugin is not supported by the browser.', + details: null, + ); + } + } +} diff --git a/federated_plugin/federated_plugin_web/pubspec.yaml b/federated_plugin/federated_plugin_web/pubspec.yaml new file mode 100644 index 0000000..42f574a --- /dev/null +++ b/federated_plugin/federated_plugin_web/pubspec.yaml @@ -0,0 +1,32 @@ +name: federated_plugin_web +description: Web implementation of federated_plugin to retrieve current battery level. +version: 0.0.1 +publish_to: none + +environment: + sdk: ^3.7.0-0 + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + federated_plugin_platform_interface: + path: ../federated_plugin_platform_interface + web: ^1.1.0 + +dev_dependencies: + analysis_defaults: + path: ../../../analysis_defaults + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + mockito: ^5.0.2 + +flutter: + plugin: + platforms: + web: + pluginClass: FederatedPlugin + fileName: federated_plugin_web.dart diff --git a/federated_plugin/federated_plugin_web/test_driver/federated_plugin_web_integration_test.dart b/federated_plugin/federated_plugin_web/test_driver/federated_plugin_web_integration_test.dart new file mode 100644 index 0000000..ca0d54c --- /dev/null +++ b/federated_plugin/federated_plugin_web/test_driver/federated_plugin_web_integration_test.dart @@ -0,0 +1,9 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() async => integrationDriver(); diff --git a/federated_plugin/federated_plugin_web/web/index.html b/federated_plugin/federated_plugin_web/web/index.html new file mode 100644 index 0000000..2cf1b6a --- /dev/null +++ b/federated_plugin/federated_plugin_web/web/index.html @@ -0,0 +1,10 @@ + + + + + Browser Tests + + + + + diff --git a/federated_plugin/federated_plugin_windows/.gitignore b/federated_plugin/federated_plugin_windows/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/federated_plugin/federated_plugin_windows/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/federated_plugin/federated_plugin_windows/.metadata b/federated_plugin/federated_plugin_windows/.metadata new file mode 100644 index 0000000..8c15ad7 --- /dev/null +++ b/federated_plugin/federated_plugin_windows/.metadata @@ -0,0 +1,10 @@ +# 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: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: plugin diff --git a/federated_plugin/federated_plugin_windows/analysis_options.yaml b/federated_plugin/federated_plugin_windows/analysis_options.yaml new file mode 100644 index 0000000..13d6fe1 --- /dev/null +++ b/federated_plugin/federated_plugin_windows/analysis_options.yaml @@ -0,0 +1 @@ +include: package:analysis_defaults/flutter.yaml diff --git a/federated_plugin/federated_plugin_windows/example/README.md b/federated_plugin/federated_plugin_windows/example/README.md new file mode 100644 index 0000000..94cbe1d --- /dev/null +++ b/federated_plugin/federated_plugin_windows/example/README.md @@ -0,0 +1,3 @@ +# federated_plugin_windows_example + +To view the usage of plugin, head over to [federated_plugin/example](../../federated_plugin/example). diff --git a/federated_plugin/federated_plugin_windows/lib/federated_plugin_windows.dart b/federated_plugin/federated_plugin_windows/lib/federated_plugin_windows.dart new file mode 100644 index 0000000..136d1d6 --- /dev/null +++ b/federated_plugin/federated_plugin_windows/lib/federated_plugin_windows.dart @@ -0,0 +1,2 @@ +// The federated_plugin_windows uses the default BatteryMethodChannel used by +// federated_plugin_platform_interface to do platform calls. diff --git a/federated_plugin/federated_plugin_windows/pubspec.yaml b/federated_plugin/federated_plugin_windows/pubspec.yaml new file mode 100644 index 0000000..bc67592 --- /dev/null +++ b/federated_plugin/federated_plugin_windows/pubspec.yaml @@ -0,0 +1,23 @@ +name: federated_plugin_windows +description: Windows implementation of federated_plugin to retrieve current battery level. +version: 0.0.1 +homepage: + +environment: + sdk: ^3.7.0-0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + analysis_defaults: + path: ../../../analysis_defaults + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + windows: + pluginClass: FederatedPluginWindowsPlugin diff --git a/federated_plugin/federated_plugin_windows/windows/.gitignore b/federated_plugin/federated_plugin_windows/windows/.gitignore new file mode 100644 index 0000000..b3eb2be --- /dev/null +++ b/federated_plugin/federated_plugin_windows/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/federated_plugin/federated_plugin_windows/windows/CMakeLists.txt b/federated_plugin/federated_plugin_windows/windows/CMakeLists.txt new file mode 100644 index 0000000..94b8c64 --- /dev/null +++ b/federated_plugin/federated_plugin_windows/windows/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.14) +set(PROJECT_NAME "federated_plugin_windows") +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "federated_plugin_windows_plugin") + +add_library(${PLUGIN_NAME} SHARED + "federated_plugin_windows_plugin.cpp" +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +# List of absolute paths to libraries that should be bundled with the plugin +set(federated_plugin_windows_bundled_libraries + "" + PARENT_SCOPE +) diff --git a/federated_plugin/federated_plugin_windows/windows/federated_plugin_windows_plugin.cpp b/federated_plugin/federated_plugin_windows/windows/federated_plugin_windows_plugin.cpp new file mode 100644 index 0000000..3348771 --- /dev/null +++ b/federated_plugin/federated_plugin_windows/windows/federated_plugin_windows_plugin.cpp @@ -0,0 +1,89 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "include/federated_plugin_windows/federated_plugin_windows_plugin.h" + +// This must be included before many other Windows headers. +#include + +#include +#include +#include + +#include +#include + +namespace { + +class FederatedPluginWindowsPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + + FederatedPluginWindowsPlugin(); + + virtual ~FederatedPluginWindowsPlugin(); + + private: + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); +}; + +// static +void FederatedPluginWindowsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows *registrar) { + auto channel = + std::make_unique>( + registrar->messenger(), "battery", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto &call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +FederatedPluginWindowsPlugin::FederatedPluginWindowsPlugin() {} + +FederatedPluginWindowsPlugin::~FederatedPluginWindowsPlugin() {} + +void FederatedPluginWindowsPlugin::HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result) { + if (method_call.method_name().compare("getBatteryLevel") == 0) { + SYSTEM_POWER_STATUS systemPower; + // GetSystemPowerStatus will retrieve the power status of the system. + if (GetSystemPowerStatus(&systemPower)) { + int batteryLevel = systemPower.BatteryLifePercent; + // The batteryLevel value in the range 0 to 100, or 255 if status is unknown. + if (batteryLevel != 255) { + flutter::EncodableValue response(batteryLevel); + result->Success(&response); + } + else { + result->Error("STATUS_UNAVAILABLE", "Not able to determine battery level."); + } + } + else { + result->Error("STATUS_UNAVAILABLE", "Not able to determine battery level."); + } + } + else { + result->NotImplemented(); + } +} + +} // namespace + +void FederatedPluginWindowsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + FederatedPluginWindowsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/federated_plugin/federated_plugin_windows/windows/include/federated_plugin_windows/federated_plugin_windows_plugin.h b/federated_plugin/federated_plugin_windows/windows/include/federated_plugin_windows/federated_plugin_windows_plugin.h new file mode 100644 index 0000000..0becf0c --- /dev/null +++ b/federated_plugin/federated_plugin_windows/windows/include/federated_plugin_windows/federated_plugin_windows_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_FEDERATED_PLUGIN_WINDOWS_PLUGIN_H_ +#define FLUTTER_PLUGIN_FEDERATED_PLUGIN_WINDOWS_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void FederatedPluginWindowsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FEDERATED_PLUGIN_WINDOWS_PLUGIN_H_ diff --git a/firebase_ai_live_api_demo/.gitignore b/firebase_ai_live_api_demo/.gitignore new file mode 100644 index 0000000..1c4e783 --- /dev/null +++ b/firebase_ai_live_api_demo/.gitignore @@ -0,0 +1,54 @@ +# 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 +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# 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 + +# macOS +ephemeral/ + +#firebase +firebase_options.dart +google-services.json +GoogleService-Info.plist +firebase.json diff --git a/firebase_ai_live_api_demo/.metadata b/firebase_ai_live_api_demo/.metadata new file mode 100644 index 0000000..6a623a4 --- /dev/null +++ b/firebase_ai_live_api_demo/.metadata @@ -0,0 +1,45 @@ +# 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: "d7b523b356d15fb81e7d340bbe52b47f93937323" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: android + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: ios + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: linux + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: macos + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: web + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: windows + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + + # 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' diff --git a/firebase_ai_live_api_demo/README.md b/firebase_ai_live_api_demo/README.md new file mode 100644 index 0000000..5bab3bc --- /dev/null +++ b/firebase_ai_live_api_demo/README.md @@ -0,0 +1,78 @@ +# Firebase AI Logic Live API Demo +**Target Platforms:** iOS, Android, Web + +**Tech Stack:** [Flutter](https://flutter.dev/) (frontend), [Firebase AI Logic](https://firebase.google.com/docs/ai-logic) (Gemini API in Vertex AI for the backend) + +![Plant Identifier – Firebase AI Live API Demo App on mobile & web](README/PlantIdentifierHero.png) + +This app demonstrates how to build a Flutter app with real-time bidirectional +audio & video streaming to the Gemini "Live API" using Firebase AI Logic. + +As seen in as seen in the +["What's New in Flutter"](https://youtu.be/v6Rzo5khNE8?si=0316B2O7xDM4Zp4S&t=2278) +Google I/O 2025 keynote. + +## Getting Started + +1. Follow [these instructions](https://firebase.google.com/docs/ai-logic/get-started?&api=vertex#set-up-firebase) +to set up a Firebase project + +1. Connect the app to your Firebase project by using `flutterfire configure`. + +Install `flutterfire_cli`: + +```console +flutter pub global activate flutterfire_cli +``` + +Then run the `flutterfire` command to configure this project for your Firebase project: + +```console +rm lib/firebase_options.dart +flutterfire configure +``` + +1. Run `flutter pub get` in the root of the project directory `firebase_ai_live_api_demo` to +install the Flutter app dependencies + +1. Run `flutter run -d ` to start the app on iOS, Android, or Web. + +> [!TIP] +> Get available devices by running `flutter devices` ex: `AA8A7357`, `macos`, `chrome`. +> The live video functionality won't work on iOS simulators due to camera restrictions. + +## How to use the demo app + +1. When prompted, allow the app permission to access your camera and microphone. + +1. Click the call button and say "Hello Gemini!" + +2. Turn on your device camera by clicking the camera button. + +1. You'll see in [`lib/src/flutterfire_ai_live_api_demo.dart`](https://github.com/flutter/demos/blob/bf2a43a5c3294cab00beef093be857544c2aad08/firebase_ai_live_api_demo/lib/src/flutterfire_ai_live_api_demo.dart#L24) that the app +is pre-configured with a "plant identifier" system instruction. So point your camera at a plant and ask Gemini to identify it! + +```dart +systemInstruction: Content.text( + 'You are a plant identifier. Greet the user by telling them that you ' + 'are a plant identifier. Ask them to turn on their camera and show ' + 'you a plant and you can help them identify plants and flowers. ' + 'Your job is to help the user dentify plants and flowers. ' + 'When the user asks you to identify a plant or flower, respond ' + 'by telling them what it is and along with fun fact about it. ' + 'If you\'re unable to identify the plant or flower, you may ask the user ' + 'for more information about it or ask for a closer look.', +) +``` + +If you have some time on your hands, try to modify the system instruction, +model configuration, or add tools that let Gemini retrieve real-time info +or take some sort of action. + +## Additional Resources +- [[Codelab] Build a Gemini powered Flutter app with Flutter & Firebase AI Logic](https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist) +- [Firebase AI Logic docs](https://firebase.google.com/docs/ai-logic) + +Feeling inspired? Check out these other Flutter & Firebase AI Logic sample apps! +- [Agentic App Manager](https://github.com/flutter/demos/tree/main/agentic_app_manager): Build an agentic experience in a Flutter app using Firebase AI Logic with the Gemini API in Vertex AI. +- [Colorist](https://github.com/flutter/demos/tree/main/vertex_ai_firebase_flutter_app): Explore LLM tooling interfaces by allowing users to describe colors in natural language. The app uses Gemini LLM to interpret descriptions and change the color of a displayed square by calling specialized color tools. diff --git a/firebase_ai_live_api_demo/README/PlantIdentifierHero.png b/firebase_ai_live_api_demo/README/PlantIdentifierHero.png new file mode 100644 index 0000000..136df7a Binary files /dev/null and b/firebase_ai_live_api_demo/README/PlantIdentifierHero.png differ diff --git a/firebase_ai_live_api_demo/analysis_options.yaml b/firebase_ai_live_api_demo/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/firebase_ai_live_api_demo/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/firebase_ai_live_api_demo/android/.gitignore b/firebase_ai_live_api_demo/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/firebase_ai_live_api_demo/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/firebase_ai_live_api_demo/android/app/build.gradle.kts b/firebase_ai_live_api_demo/android/app/build.gradle.kts new file mode 100644 index 0000000..edca173 --- /dev/null +++ b/firebase_ai_live_api_demo/android/app/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.flutterfire_ai_live_api_demo" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.flutterfire_ai_live_api_demo" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + ndkVersion = "27.0.12077973" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/firebase_ai_live_api_demo/android/app/src/debug/AndroidManifest.xml b/firebase_ai_live_api_demo/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/firebase_ai_live_api_demo/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/firebase_ai_live_api_demo/android/app/src/main/AndroidManifest.xml b/firebase_ai_live_api_demo/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..35c4dda --- /dev/null +++ b/firebase_ai_live_api_demo/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_live_api_demo/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt b/firebase_ai_live_api_demo/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt new file mode 100644 index 0000000..ff98253 --- /dev/null +++ b/firebase_ai_live_api_demo/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.flutterfire_ai_live_api_demo + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/firebase_ai_live_api_demo/android/app/src/main/res/drawable-v21/launch_background.xml b/firebase_ai_live_api_demo/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/firebase_ai_live_api_demo/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/firebase_ai_live_api_demo/android/app/src/main/res/drawable/launch_background.xml b/firebase_ai_live_api_demo/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/firebase_ai_live_api_demo/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/firebase_ai_live_api_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/firebase_ai_live_api_demo/android/app/src/main/res/values-night/styles.xml b/firebase_ai_live_api_demo/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/firebase_ai_live_api_demo/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/firebase_ai_live_api_demo/android/app/src/main/res/values/styles.xml b/firebase_ai_live_api_demo/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/firebase_ai_live_api_demo/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/firebase_ai_live_api_demo/android/app/src/profile/AndroidManifest.xml b/firebase_ai_live_api_demo/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/firebase_ai_live_api_demo/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/firebase_ai_live_api_demo/android/build.gradle.kts b/firebase_ai_live_api_demo/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/firebase_ai_live_api_demo/android/build.gradle.kts @@ -0,0 +1,21 @@ +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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/firebase_ai_live_api_demo/android/gradle.properties b/firebase_ai_live_api_demo/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/firebase_ai_live_api_demo/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/firebase_ai_live_api_demo/android/gradle/wrapper/gradle-wrapper.properties b/firebase_ai_live_api_demo/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afa1e8e --- /dev/null +++ b/firebase_ai_live_api_demo/android/gradle/wrapper/gradle-wrapper.properties @@ -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.10.2-all.zip diff --git a/firebase_ai_live_api_demo/android/settings.gradle.kts b/firebase_ai_live_api_demo/android/settings.gradle.kts new file mode 100644 index 0000000..9e2d35c --- /dev/null +++ b/firebase_ai_live_api_demo/android/settings.gradle.kts @@ -0,0 +1,28 @@ +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.7.0" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/firebase_ai_live_api_demo/ios/.gitignore b/firebase_ai_live_api_demo/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +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 diff --git a/firebase_ai_live_api_demo/ios/Flutter/AppFrameworkInfo.plist b/firebase_ai_live_api_demo/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/firebase_ai_live_api_demo/ios/Flutter/Debug.xcconfig b/firebase_ai_live_api_demo/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/firebase_ai_live_api_demo/ios/Flutter/Release.xcconfig b/firebase_ai_live_api_demo/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/firebase_ai_live_api_demo/ios/Podfile b/firebase_ai_live_api_demo/ios/Podfile new file mode 100644 index 0000000..25e6a16 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' #FlutterFire plugins require minimum of iOS 13 https://firebase.google.com/docs/flutter/setup?platform=ios + +# 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 diff --git a/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.pbxproj b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a575159 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,735 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2E93BD09C0B0FFDB52311EAA /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3760F5D70F4BD5A3F3B08C19 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + EF7DAC43BEEDC80B2E3D9979 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */; }; +/* 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 */ + 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2B33F176EE317BBD897D8C8E /* 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 = ""; }; + 2D957529EFAEE0BDD3A76F18 /* 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8E6CD9C6541A3CF46C64E1C9 /* 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 = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + BA6D30E40E8F68B06E56229C /* 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 = ""; }; + E9425AF2FD1A44E278C01648 /* 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 = ""; }; + F9CFD1A5C79D4AABA9C9A108 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 37FD498E274777199498BC21 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2E93BD09C0B0FFDB52311EAA /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3760F5D70F4BD5A3F3B08C19 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3044D5D4F7C58D0A98D41E15 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */, + 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 6B0AAB17435D3F0DE252CF55 /* Pods */ = { + isa = PBXGroup; + children = ( + BA6D30E40E8F68B06E56229C /* Pods-Runner.debug.xcconfig */, + F9CFD1A5C79D4AABA9C9A108 /* Pods-Runner.release.xcconfig */, + 8E6CD9C6541A3CF46C64E1C9 /* Pods-Runner.profile.xcconfig */, + 2B33F176EE317BBD897D8C8E /* Pods-RunnerTests.debug.xcconfig */, + E9425AF2FD1A44E278C01648 /* Pods-RunnerTests.release.xcconfig */, + 2D957529EFAEE0BDD3A76F18 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 6B0AAB17435D3F0DE252CF55 /* Pods */, + 3044D5D4F7C58D0A98D41E15 /* Frameworks */, + 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 04715B1AF939C799C4D37CEC /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 37FD498E274777199498BC21 /* 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 = ( + E56B7B1A5FD7695313B2FE04 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E65799E83B05311337559E13 /* [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 = ( + ); + 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 */, + EF7DAC43BEEDC80B2E3D9979 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 04715B1AF939C799C4D37CEC /* [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"; + }; + E56B7B1A5FD7695313B2FE04 /* [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; + }; + E65799E83B05311337559E13 /* [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 */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + 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 = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2B33F176EE317BBD897D8C8E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E9425AF2FD1A44E278C01648 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D957529EFAEE0BDD3A76F18 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + 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; + "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 = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + 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 = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */; +} diff --git a/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/firebase_ai_live_api_demo/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..15cada4 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_live_api_demo/ios/Runner.xcworkspace/contents.xcworkspacedata b/firebase_ai_live_api_demo/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/firebase_ai_live_api_demo/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_live_api_demo/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_live_api_demo/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/firebase_ai_live_api_demo/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/firebase_ai_live_api_demo/ios/Runner/AppDelegate.swift b/firebase_ai_live_api_demo/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/firebase_ai_live_api_demo/ios/Runner/Base.lproj/LaunchScreen.storyboard b/firebase_ai_live_api_demo/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_live_api_demo/ios/Runner/Base.lproj/Main.storyboard b/firebase_ai_live_api_demo/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_live_api_demo/ios/Runner/Info.plist b/firebase_ai_live_api_demo/ios/Runner/Info.plist new file mode 100644 index 0000000..27c1041 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Multimodal Ai Prototype + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutterfire_ai_live_api_demo + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSMicrophoneUsageDescription + Need to record user voice to communicate with Gemini + NSCameraUsageDescription + Need camera to show Gemini live video stream + + diff --git a/firebase_ai_live_api_demo/ios/Runner/Runner-Bridging-Header.h b/firebase_ai_live_api_demo/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/firebase_ai_live_api_demo/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/firebase_ai_live_api_demo/ios/RunnerTests/RunnerTests.swift b/firebase_ai_live_api_demo/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/firebase_ai_live_api_demo/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/firebase_ai_live_api_demo/lib/main.dart b/firebase_ai_live_api_demo/lib/main.dart new file mode 100644 index 0000000..74e58b7 --- /dev/null +++ b/firebase_ai_live_api_demo/lib/main.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:firebase_core/firebase_core.dart'; +import './firebase_options.dart'; +import 'src/flutterfire_ai_live_api_demo.dart'; +import './src/ui_components/ui_components.dart'; + +FirebaseOptions options = DefaultFirebaseOptions.currentPlatform; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: options); + + runApp(const ProviderScope(child: MyApp())); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter x Firebase AI Live API Demo', + home: FlutterFireAILiveAPIDemo(), // Demo app here + theme: themeData, + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/firebase_ai_live_api_demo/lib/src/flutterfire_ai_live_api_demo.dart b/firebase_ai_live_api_demo/lib/src/flutterfire_ai_live_api_demo.dart new file mode 100644 index 0000000..5754a6d --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/flutterfire_ai_live_api_demo.dart @@ -0,0 +1,321 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import './ui_components/ui_components.dart'; +import '../src/providers.dart'; + +class FlutterFireAILiveAPIDemo extends ConsumerStatefulWidget { + const FlutterFireAILiveAPIDemo({super.key}); + + @override + ConsumerState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends ConsumerState { + // Flag(s) to prevent multiple stream initializations + bool _audioIsInitialized = false; + bool _videoIsInitialized = false; + final LiveGenerativeModel + _liveModel = FirebaseAI.vertexAI().liveGenerativeModel( + model: 'gemini-2.0-flash-live-preview-04-09', + systemInstruction: Content.text( + 'You are a plant identifier. Greet the user by telling them that you ' + 'are a plant identifier. Ask them to turn on their camera and show ' + 'you a plant and you can help them identify plants and flowers. ' + 'Your job is to help the user dentify plants and flowers. ' + 'When the user asks you to identify a plant or flower, respond ' + 'by telling them what it is and along with fun fact about it. ' + 'If you\'re unable to identify the plant or flower, you may ask the user ' + 'for more information about it or ask for a closer look.', + ), + liveGenerationConfig: LiveGenerationConfig( + speechConfig: SpeechConfig(voiceName: 'fenrir'), + responseModalities: [ResponseModalities.audio], + ), + ); + late LiveSession _session; // Gemini Live Session + bool _settingUpLiveSession = false; // Session is getting set up + bool _liveSessionIsOpen = false; // Session is open and ready to go. + bool _audioStreamIsActive = + false; // Session is running and with audio input & output streams active + bool _cameraIsActive = false; // Whether sending video stream to Gemini + + @override + void initState() { + super.initState(); + // Load the first frame AND THEN initialize audio & video setup + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeAudio(); + _initializeVideo(); + }); + } + + @override + void dispose() { + super.dispose(); + ref.read(audioInputProvider).dispose(); + ref.read(audioOutputProvider).dispose(); + ref.read(videoInputProvider).dispose(); + if (_liveSessionIsOpen) { + unawaited(_session.close()); // Ensure session is closed on dispose + } + } + + /// AUDIO INPUT & OUTPUT + Future _initializeAudio() async { + try { + await ref.read(audioInputProvider).init(); // Initialize Audio Input + await ref.read(audioOutputProvider).init(); // Initialize Audio Output + + setState(() { + _audioIsInitialized = true; + }); + } catch (e) { + log("Error during audio initialization: $e"); + if (!mounted) return; + + var errorSnackBar = SnackBar( + content: Text('Oops! Something went wrong with the audio setup.'), + action: SnackBarAction(label: 'Retry', onPressed: _initializeAudio), + ); + ScaffoldMessenger.of(context).showSnackBar(errorSnackBar); + } + } + + void toggleAudioStream() async { + _audioStreamIsActive ? await stopAudioStream() : await startAudioStream(); + } + + Future startAudioStream() async { + // Start the Gemini Live session + await _toggleLiveSession(); + + final audioInput = ref.read(audioInputProvider); + final audioOutput = ref.read(audioOutputProvider); + + // Start recording audio input stream + var audioInputStream = await audioInput.startRecordingStream(); + log('Audio input stream is recording!'); + + // Start playing audio output stream + await audioOutput.playStream(); + log('Audio output stream is playing!'); + + setState(() { + _audioStreamIsActive = true; + }); + + // Wrap input stream audio data in InlineDataPart and send to Gemini session + _session.sendMediaStream( + audioInputStream.map((data) { + return InlineDataPart('audio/pcm', data); + }), + ); + } + + Future stopAudioStream() async { + // If sending video, stop recording & transmitting. + if (_cameraIsActive) { + stopVideoStream(); + } + // Stop recording audio input + await ref.read(audioInputProvider).stopRecording(); + + // Stop playing audio output + await ref.read(audioOutputProvider).stopStream(); + + // End the Gemini live session + await _toggleLiveSession(); + + setState(() { + _audioStreamIsActive = false; + }); + } + + Future toggleMuteInput() async { + await ref.read(audioInputProvider).togglePauseRecording(); + } + + /// VIDEO INPUT + Future _initializeVideo() async { + try { + await ref.read(videoInputProvider).init(); + setState(() { + _videoIsInitialized = true; + }); + } catch (e) { + log("Error during video initialization: $e"); + } + } + + void startVideoStream() { + if (!_videoIsInitialized || !_audioStreamIsActive || _cameraIsActive) { + return; + } + + Stream imageStream = ref + .read(videoInputProvider) + .startStreamingImages(); + + // Wrap video input stream image data in InlineDataPart and send to Gemini session + _session.sendMediaStream( + imageStream.map((data) { + return InlineDataPart("image/jpeg", data); + }), + ); + + setState(() { + _cameraIsActive = true; + }); + } + + void stopVideoStream() async { + await ref.read(videoInputProvider).stopStreamingImages(); + setState(() { + _cameraIsActive = false; + }); + } + + void toggleVideoStream() async { + _cameraIsActive ? stopVideoStream() : startVideoStream(); + } + + /// Firebase AI Logic + Future _toggleLiveSession() async { + setState(() { + _settingUpLiveSession = true; + }); + + if (!_liveSessionIsOpen) { + _session = await _liveModel.connect(); + _liveSessionIsOpen = true; + unawaited(processMessagesContinuously()); + } else { + await _session.close(); + _liveSessionIsOpen = false; + } + + setState(() { + _settingUpLiveSession = false; + }); + } + + Future processMessagesContinuously() async { + try { + await for (final response in _session.receive()) { + // Process the received message + LiveServerMessage message = response.message; + await _handleLiveServerMessage(message); + } + log('Live session receive stream completed.'); + } catch (e) { + log('Error receiving live session messages: $e'); + } + } + + Future _handleLiveServerMessage(LiveServerMessage response) async { + if (response is LiveServerContent) { + if (response.modelTurn != null) { + await _handleLiveServerContent(response); + } + if (response.turnComplete != null && response.turnComplete!) { + await _handleTurnComplete(); + } + if (response.interrupted != null && response.interrupted!) { + log('Interrupted: $response'); + } + } + + if (response is LiveServerToolCall && response.functionCalls != null) { + await _handleLiveServerToolCall(response); + } + } + + Future _handleLiveServerContent(LiveServerContent response) async { + final partList = response.modelTurn?.parts; + if (partList != null) { + for (final part in partList) { + switch (part) { + case TextPart textPart: + await _handleTextPart(textPart); + case InlineDataPart inlineDataPart: + await _handleInlineDataPart(inlineDataPart); + default: + log('Received part with type ${part.runtimeType}'); + } + } + } + } + + Future _handleInlineDataPart(InlineDataPart part) async { + if (part.mimeType.startsWith('audio')) { + // If DataPart is audio, add it to the output audio stream + ref.read(audioOutputProvider).addDataToAudioStream(part.bytes); + } + } + + Future _handleTextPart(TextPart part) async { + log('Text message from Gemini: ${part.text}'); + } + + Future _handleTurnComplete() async { + log('Model is done generating. Turn complete!'); + } + + Future _handleLiveServerToolCall(LiveServerToolCall response) async { + if (response.functionCalls?.isNotEmpty ?? false) { + log("Gemini made a function call!"); + } + } + + @override + Widget build(BuildContext context) { + final audioInput = ref.watch(audioInputProvider); + final videoInput = ref.watch(videoInputProvider); + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + backgroundColor: Colors.transparent, + leadingWidth: 100, + leading: LeafAppIcon(), + title: AppTitle(title: 'FlutterFire AI Demo'), + ), + body: _cameraIsActive + ? Center( + child: FullCameraPreview(controller: videoInput.cameraController), + ) + : CenterCircle( + child: Padding( + padding: EdgeInsets.all(60), + child: _settingUpLiveSession + ? CircularProgressIndicator() + : Icon(size: 54, Icons.waves), + ), + ), + bottomNavigationBar: BottomBar( + child: Row( + children: [ + ChatButton(), + VideoButton( + isActive: _cameraIsActive, + onPressed: toggleVideoStream, + ), + const Spacer(), + MuteButton( + isMuted: audioInput.isPaused, + onPressed: _audioStreamIsActive ? toggleMuteInput : null, + ), + CallButton( + isActive: _audioStreamIsActive, + onPressed: _audioIsInitialized ? toggleAudioStream : null, + ), + ], + ), + ), + ); + } +} diff --git a/firebase_ai_live_api_demo/lib/src/providers.dart b/firebase_ai_live_api_demo/lib/src/providers.dart new file mode 100644 index 0000000..9e99c3d --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/providers.dart @@ -0,0 +1,16 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../src/utilities/audio_input.dart'; +import '../src/utilities/audio_output.dart'; +import '../src/utilities/video_input.dart'; + +final audioInputProvider = ChangeNotifierProvider((ref) { + return AudioInput(); +}); + +final videoInputProvider = Provider((ref) { + return VideoInput(); +}); + +final audioOutputProvider = Provider((ref) { + return AudioOutput(); +}); diff --git a/firebase_ai_live_api_demo/lib/src/ui_components/bottom_bar.dart b/firebase_ai_live_api_demo/lib/src/ui_components/bottom_bar.dart new file mode 100644 index 0000000..dca90fb --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/ui_components/bottom_bar.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; + +class BottomBar extends StatelessWidget { + const BottomBar({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainer, + ), + ), + ), + child: Padding(padding: EdgeInsets.all(16), child: child), + ), + ); + } +} + +class ChatButton extends StatelessWidget { + const ChatButton({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(4), + child: IconButton.filledTonal( + onPressed: () {}, + icon: Padding( + padding: EdgeInsets.all(4), + child: Icon(Icons.chat_bubble_outline), + ), + ), + ); + } +} + +class VideoButton extends StatelessWidget { + const VideoButton({required this.isActive, this.onPressed, super.key}); + + final bool isActive; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(4), + child: IconButton.filledTonal( + style: isActive + ? ButtonStyle( + backgroundColor: WidgetStateProperty.all( + const Color.fromARGB(240, 238, 255, 244), + ), + iconColor: WidgetStateProperty.all(Colors.black87), + ) + : ButtonStyle(backgroundColor: null), + onPressed: onPressed, + icon: Padding( + padding: EdgeInsets.all(4), + child: Icon(Icons.video_call_rounded), + ), + ), + ); + } +} + +class MuteButton extends StatelessWidget { + const MuteButton({required this.isMuted, this.onPressed, super.key}); + + final bool isMuted; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(4), + child: IconButton.filledTonal( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isMuted ? null : const Color.fromARGB(240, 238, 255, 244), + ), + ), + onPressed: onPressed, + icon: Padding( + padding: EdgeInsets.all(4), + child: isMuted + ? Icon(Icons.mic_off) + : Icon(color: Colors.black87, Icons.mic_none), + ), + ), + ); + } +} + +class CallButton extends StatelessWidget { + const CallButton({required this.isActive, this.onPressed, super.key}); + + final bool isActive; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(4), + child: IconButton.filledTonal( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isActive + ? const Color.fromARGB(255, 199, 39, 27) + : Colors.green[500], + ), + ), + onPressed: onPressed, + icon: Padding( + padding: EdgeInsets.all(4), + child: Icon(isActive ? Icons.phone_disabled_outlined : Icons.phone), + ), + ), + ); + } +} diff --git a/firebase_ai_live_api_demo/lib/src/ui_components/branding.dart b/firebase_ai_live_api_demo/lib/src/ui_components/branding.dart new file mode 100644 index 0000000..0a8babd --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/ui_components/branding.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class AppTitle extends StatelessWidget { + const AppTitle({required this.title, super.key}); + + final String title; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 16), + child: Text( + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + title, + ), + ); + } +} + +class LeafAppIcon extends StatelessWidget { + const LeafAppIcon({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(left: 16, top: 16), + child: Container( + decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle), + child: Icon(color: Colors.green[600], Icons.spa), + ), + ); + } +} diff --git a/firebase_ai_live_api_demo/lib/src/ui_components/camera_previews.dart b/firebase_ai_live_api_demo/lib/src/ui_components/camera_previews.dart new file mode 100644 index 0000000..296902a --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/ui_components/camera_previews.dart @@ -0,0 +1,72 @@ +import 'package:camera/camera.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter/material.dart'; + +class SquareCameraPreview extends StatelessWidget { + const SquareCameraPreview({required this.controller, super.key}); + + final CameraController controller; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 350, + height: 350, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), + child: AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + // The camera preview is often not a square. To fill the 1:1 aspect + // ratio, we scale the preview to cover the area and clip it. + child: Transform.scale( + scale: controller.value.aspectRatio / 1, + child: Center(child: CameraPreview(controller)), + ), + ), + ), + ), + ); + } +} + +class FullCameraPreview extends StatefulWidget { + const FullCameraPreview({required this.controller, super.key}); + + final CameraController controller; + + @override + State createState() => _FullCameraPreviewState(); +} + +class _FullCameraPreviewState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animController; + + @override + void initState() { + super.initState(); + _animController = AnimationController( + vsync: this, // the SingleTickerProviderStateMixin + duration: Duration(seconds: 1), + ); + } + + @override + void dispose() { + super.dispose(); + _animController.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(16), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(16)), + child: CameraPreview(widget.controller), + ), + ).animate(controller: _animController).scaleXY().fadeIn(); + } +} diff --git a/firebase_ai_live_api_demo/lib/src/ui_components/sound_waves.dart b/firebase_ai_live_api_demo/lib/src/ui_components/sound_waves.dart new file mode 100644 index 0000000..95bdc41 --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/ui_components/sound_waves.dart @@ -0,0 +1,78 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class CenterCircle extends StatelessWidget { + const CenterCircle({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Center( + child: CustomPaint( + size: const Size(160, 160), + painter: NestedCirclesPainter( + color: Theme.of(context).colorScheme.primary, + strokeWidth: 1.0, + gapBetweenCircles: 4.0, + ), + child: child, + ), + ); + } +} + +// Custom Painter for drawing two nested circles +class NestedCirclesPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double gapBetweenCircles; // The space between the two circles + + NestedCirclesPainter({ + this.color = Colors.white54, // Default color for the circles + this.strokeWidth = 1.5, // Default stroke width for both circles + this.gapBetweenCircles = 4.0, // Default gap between the circles + }); + + @override + void paint(Canvas canvas, Size size) { + // Calculate the center of the drawing area + final Offset center = Offset(size.width / 2, size.height / 2); + + // Configure the paint properties (same for both circles) + final Paint paint = Paint() + ..color = color + .withValues(alpha: 0.7) // Make circles slightly transparent + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; // Draw the outline + + // Calculate the radius for the outer circle + // Ensure it fits within the bounds defined by 'size' + final double outerRadius = + min(size.width / 2, size.height / 2) - strokeWidth / 2; + + // Calculate the radius for the inner circle + final double innerRadius = + outerRadius - gapBetweenCircles - strokeWidth / 2; + + // Ensure inner radius is not negative + if (innerRadius > 0) { + // Draw the outer circle + canvas.drawCircle(center, outerRadius, paint); + // Draw the inner circle + canvas.drawCircle(center, innerRadius, paint); + } else { + // If the gap is too large, just draw the outer circle + canvas.drawCircle(center, outerRadius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + // Repaint only if properties change + return oldDelegate is NestedCirclesPainter && + (oldDelegate.color != color || + oldDelegate.strokeWidth != strokeWidth || + oldDelegate.gapBetweenCircles != gapBetweenCircles); + } +} diff --git a/firebase_ai_live_api_demo/lib/src/ui_components/theme.dart b/firebase_ai_live_api_demo/lib/src/ui_components/theme.dart new file mode 100644 index 0000000..866f717 --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/ui_components/theme.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +ThemeData themeData = ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Color.fromARGB(255, 18, 69, 2), + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.tonalSpot, + ).copyWith(surface: Color.fromARGB(255, 7, 41, 21)), +); diff --git a/firebase_ai_live_api_demo/lib/src/ui_components/ui_components.dart b/firebase_ai_live_api_demo/lib/src/ui_components/ui_components.dart new file mode 100644 index 0000000..4db301e --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/ui_components/ui_components.dart @@ -0,0 +1,5 @@ +export 'branding.dart'; +export 'bottom_bar.dart'; +export 'camera_previews.dart'; +export 'sound_waves.dart'; +export 'theme.dart'; diff --git a/firebase_ai_live_api_demo/lib/src/utilities/audio_input.dart b/firebase_ai_live_api_demo/lib/src/utilities/audio_input.dart new file mode 100644 index 0000000..1a1ba0b --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/utilities/audio_input.dart @@ -0,0 +1,84 @@ +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:record/record.dart'; + +class AudioInput extends ChangeNotifier { + final _recorder = AudioRecorder(); + RecordConfig recordConfig = RecordConfig( + encoder: AudioEncoder.pcm16bits, + sampleRate: 24000, + numChannels: 1, + echoCancel: true, + noiseSuppress: true, + androidConfig: AndroidRecordConfig( + audioSource: AndroidAudioSource.voiceCommunication, + ), + iosConfig: IosRecordConfig(categoryOptions: []), + ); + bool isRecording = false; + bool isPaused = false; + + Future init() async { + await checkPermission(); + } + + @override + void dispose() { + _recorder.dispose(); + super.dispose(); + } + + Future checkPermission() async { + final hasPermission = await _recorder.hasPermission(); + if (!hasPermission) { + throw MicrophonePermissionDeniedException( + 'This app does not have microphone permissions. Please enable it.', + ); + } + } + + Future> startRecordingStream() async { + final devices = await _recorder.listInputDevices(); + log(devices.toString()); + // Make audioStream a local variable and convert to a broadcast stream. + final audioStream = (await _recorder.startStream( + recordConfig, + )).asBroadcastStream(); + isRecording = true; + //print("${isRecording ? "Is" : "Not"} Recording"); + notifyListeners(); + return audioStream; + } + + Future stopRecording() async { + await _recorder.stop(); + isRecording = false; + //print("${isRecording ? "Is" : "Not"} Recording"); + notifyListeners(); + } + + Future togglePauseRecording() async { + isPaused ? await _recorder.resume() : await _recorder.pause(); + isPaused = !isPaused; + notifyListeners(); + return; + } +} + +/// An exception thrown when microphone permission is denied or not granted. +class MicrophonePermissionDeniedException implements Exception { + /// The optional message associated with the permission denial. + final String? message; + + /// Creates a new [MicrophonePermissionDeniedException] with an optional [message]. + MicrophonePermissionDeniedException([this.message]); + + @override + String toString() { + if (message == null) { + return 'MicrophonePermissionDeniedException'; + } + return 'MicrophonePermissionDeniedException: $message'; + } +} diff --git a/firebase_ai_live_api_demo/lib/src/utilities/audio_output.dart b/firebase_ai_live_api_demo/lib/src/utilities/audio_output.dart new file mode 100644 index 0000000..35cc4e0 --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/utilities/audio_output.dart @@ -0,0 +1,86 @@ +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:flutter_soloud/flutter_soloud.dart'; + +class AudioOutput { + var initialized = false; + AudioSource? stream; + SoundHandle? handle; + final int sampleRate = 24000; + final Channels channels = Channels.mono; + final BufferType format = BufferType.s16le; // pcm16bits + + Future init() async { + if (initialized) { + return; + } + + /// Initialize the player (singleton). + await SoLoud.instance.init(sampleRate: sampleRate, channels: channels); + initialized = true; + } + + Future dispose() async { + if (initialized) { + SoLoud.instance.disposeAllSources(); + SoLoud.instance.deinit(); + initialized = false; + } + } + + SoLoud get instance => SoLoud.instance; + + AudioSource? setupNewStream() { + if (!SoLoud.instance.isInitialized) { + return null; + } + + stream = SoLoud.instance.setBufferStream( + maxBufferSizeBytes: + 1024 * 1024 * 10, // 10MB of max buffer (not allocated) + bufferingType: BufferingType.released, + bufferingTimeNeeds: 0, + sampleRate: sampleRate, + channels: channels, + format: format, + onBuffering: (isBuffering, handle, time) { + log('Buffering: $isBuffering, Time: $time'); + }, + ); + log("New audio output stream buffer created."); + return stream; + } + + Future playStream() async { + var myStream = setupNewStream(); + if (!SoLoud.instance.isInitialized || myStream == null) { + return null; + } + // Play audio stream + handle = await SoLoud.instance.play(myStream); + stream = myStream; + return stream; + } + + void addDataToAudioStream(Uint8List audioChunk) { + var currentStream = stream; + if (currentStream != null) { + SoLoud.instance.addAudioDataStream(currentStream, audioChunk); + } + } + + Future stopStream() async { + var currentStream = stream; + var currentHandle = handle; + + // Stream doesn't exist or handle is not valid - so nothing to stop. + if (currentStream == null || + currentHandle == null || + !SoLoud.instance.getIsValidVoiceHandle(currentHandle)) { + return; + } + // End data to stream & stop currently playing sound from handle + SoLoud.instance.setDataIsEnded(currentStream); + await SoLoud.instance.stop(currentHandle); + } +} diff --git a/firebase_ai_live_api_demo/lib/src/utilities/video_input.dart b/firebase_ai_live_api_demo/lib/src/utilities/video_input.dart new file mode 100644 index 0000000..b19f813 --- /dev/null +++ b/firebase_ai_live_api_demo/lib/src/utilities/video_input.dart @@ -0,0 +1,79 @@ +import 'dart:developer'; +import 'dart:async'; +import 'dart:typed_data'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +class VideoInput extends ChangeNotifier { + late List _cameras; + late CameraController _cameraController; + Timer? _captureTimer; // Timer for periodic capture + StreamController _imageStreamController = StreamController(); + bool _isStreaming = false; + + Future init() async { + try { + _cameras = await availableCameras(); + _cameraController = CameraController( + _cameras[0], + ResolutionPreset.veryHigh, + enableAudio: false, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + await _cameraController.initialize(); + notifyListeners(); + } catch (e) { + log('Error initializing camera: $e'); + } + } + + CameraController get cameraController => _cameraController; + + Stream startStreamingImages() { + _captureTimer = Timer.periodic( + Duration(seconds: 1), // Capture images at 1 frame per second + (timer) async { + if (!_cameraController.value.isInitialized || !_isStreaming) { + log("Stopping timer due to invalid state."); + stopStreamingImages(); + return; + } + + try { + // Prevent taking picture if already taking one + if (_cameraController.value.isTakingPicture) { + return; + } + log("Taking picture..."); + final XFile imageFile = await _cameraController.takePicture(); + Uint8List imageBytes = await imageFile.readAsBytes(); + _imageStreamController.add(imageBytes); + } catch (e) { + log('Error taking picture: $e'); + } + }, + ); + _isStreaming = true; + return _imageStreamController.stream; + } + + /// Stops the periodic image capture and closes the stream. + Future stopStreamingImages() async { + if (!_isStreaming) { + return; // Nothing to stop + } + _captureTimer?.cancel(); + await _imageStreamController.close(); + _imageStreamController = StreamController(); + _cameraController.dispose(); + init(); // Reinitialize the camera for reuse later + _isStreaming = false; + } + + @override + void dispose() { + super.dispose(); + stopStreamingImages(); + _cameraController.dispose(); + } +} diff --git a/firebase_ai_live_api_demo/macos/.gitignore b/firebase_ai_live_api_demo/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/firebase_ai_live_api_demo/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/firebase_ai_live_api_demo/macos/Flutter/Flutter-Debug.xcconfig b/firebase_ai_live_api_demo/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/firebase_ai_live_api_demo/macos/Flutter/Flutter-Release.xcconfig b/firebase_ai_live_api_demo/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/firebase_ai_live_api_demo/macos/Flutter/GeneratedPluginRegistrant.swift b/firebase_ai_live_api_demo/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..d229e3a --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import firebase_app_check +import firebase_auth +import firebase_core +import path_provider_foundation +import record_darwin + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin")) +} diff --git a/firebase_ai_live_api_demo/macos/Podfile b/firebase_ai_live_api_demo/macos/Podfile new file mode 100644 index 0000000..29c8eb3 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.14' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_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_macos_build_settings(target) + end +end diff --git a/firebase_ai_live_api_demo/macos/Runner.xcodeproj/project.pbxproj b/firebase_ai_live_api_demo/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b17ee66 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* flutterfire_ai_live_api_demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutterfire_ai_live_api_demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutterfire_ai_live_api_demo.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutterfire_ai_live_api_demo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutterfire_ai_live_api_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutterfire_ai_live_api_demo"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutterfire_ai_live_api_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutterfire_ai_live_api_demo"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutterfire_ai_live_api_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutterfire_ai_live_api_demo"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/firebase_ai_live_api_demo/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_live_api_demo/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_live_api_demo/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/firebase_ai_live_api_demo/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..4f75756 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_live_api_demo/macos/Runner.xcworkspace/contents.xcworkspacedata b/firebase_ai_live_api_demo/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/firebase_ai_live_api_demo/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_live_api_demo/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_live_api_demo/macos/Runner/AppDelegate.swift b/firebase_ai_live_api_demo/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/firebase_ai_live_api_demo/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/firebase_ai_live_api_demo/macos/Runner/Base.lproj/MainMenu.xib b/firebase_ai_live_api_demo/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_live_api_demo/macos/Runner/Configs/AppInfo.xcconfig b/firebase_ai_live_api_demo/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..9c8fc8c --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutterfire_ai_live_api_demo + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/firebase_ai_live_api_demo/macos/Runner/Configs/Debug.xcconfig b/firebase_ai_live_api_demo/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/firebase_ai_live_api_demo/macos/Runner/Configs/Release.xcconfig b/firebase_ai_live_api_demo/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/firebase_ai_live_api_demo/macos/Runner/Configs/Warnings.xcconfig b/firebase_ai_live_api_demo/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/firebase_ai_live_api_demo/macos/Runner/DebugProfile.entitlements b/firebase_ai_live_api_demo/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/firebase_ai_live_api_demo/macos/Runner/Info.plist b/firebase_ai_live_api_demo/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/firebase_ai_live_api_demo/macos/Runner/MainFlutterWindow.swift b/firebase_ai_live_api_demo/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/firebase_ai_live_api_demo/macos/Runner/Release.entitlements b/firebase_ai_live_api_demo/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/firebase_ai_live_api_demo/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/firebase_ai_live_api_demo/macos/RunnerTests/RunnerTests.swift b/firebase_ai_live_api_demo/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/firebase_ai_live_api_demo/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/firebase_ai_live_api_demo/pubspec.yaml b/firebase_ai_live_api_demo/pubspec.yaml new file mode 100644 index 0000000..92b023b --- /dev/null +++ b/firebase_ai_live_api_demo/pubspec.yaml @@ -0,0 +1,96 @@ +name: flutterfire_ai_live_api_demo +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.8.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + record: ^5.2.1 + flutter_riverpod: ^2.6.1 + flutter_soloud: ^3.1.6 + firebase_core: ^3.13.0 + camera: ^0.11.1 + flutter_animate: ^4.5.2 + firebase_ai: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/firebase_ai_live_api_demo/web/favicon.png b/firebase_ai_live_api_demo/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/firebase_ai_live_api_demo/web/favicon.png differ diff --git a/firebase_ai_live_api_demo/web/icons/Icon-192.png b/firebase_ai_live_api_demo/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/firebase_ai_live_api_demo/web/icons/Icon-192.png differ diff --git a/firebase_ai_live_api_demo/web/icons/Icon-512.png b/firebase_ai_live_api_demo/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/firebase_ai_live_api_demo/web/icons/Icon-512.png differ diff --git a/firebase_ai_live_api_demo/web/icons/Icon-maskable-192.png b/firebase_ai_live_api_demo/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/firebase_ai_live_api_demo/web/icons/Icon-maskable-192.png differ diff --git a/firebase_ai_live_api_demo/web/icons/Icon-maskable-512.png b/firebase_ai_live_api_demo/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/firebase_ai_live_api_demo/web/icons/Icon-maskable-512.png differ diff --git a/firebase_ai_live_api_demo/web/index.html b/firebase_ai_live_api_demo/web/index.html new file mode 100644 index 0000000..463a28a --- /dev/null +++ b/firebase_ai_live_api_demo/web/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + flutterfire_ai_live_api_demo + + + + + + + + + + \ No newline at end of file diff --git a/firebase_ai_live_api_demo/web/manifest.json b/firebase_ai_live_api_demo/web/manifest.json new file mode 100644 index 0000000..32cd128 --- /dev/null +++ b/firebase_ai_live_api_demo/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "flutterfire_ai_live_api_demo", + "short_name": "flutterfire_ai_live_api_demo", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/firebase_ai_logic_showcase/.gitignore b/firebase_ai_logic_showcase/.gitignore new file mode 100644 index 0000000..df0e787 --- /dev/null +++ b/firebase_ai_logic_showcase/.gitignore @@ -0,0 +1,54 @@ +# 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 +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# 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 + +# macOS +ephemeral/ + +#firebase +google-services.json +GoogleService-Info.plist +.firebaserc diff --git a/firebase_ai_logic_showcase/.idx/dev.nix b/firebase_ai_logic_showcase/.idx/dev.nix new file mode 100644 index 0000000..7d5fe2c --- /dev/null +++ b/firebase_ai_logic_showcase/.idx/dev.nix @@ -0,0 +1,61 @@ +# To learn more about how to use Nix to configure your environment +# see: https://firebase.google.com/docs/studio/customize-workspace +{ pkgs, ... }: { + # Which nixpkgs channel to use. + channel = "stable-24.05"; # or "unstable" + # Use https://search.nixos.org/packages to find packages + packages = [ + pkgs.jdk21 + pkgs.unzip + pkgs.nodejs_22 + pkgs.nodePackages.nodemon + ]; + # Sets environment variables in the workspace + env = { + # Enable AppCheck for additional security for critical endpoints. + # Follow the configuration steps in the README to set up your project. + # ENABLE_APPCHECK = "TRUE"; + LOCAL_RECOMMENDATION_SERVICE = "http://127.0.0.1:8084"; + GOOGLE_PROJECT = ""; + CLOUDSDK_CORE_PROJECT = ""; + TF_VAR_project = ""; + # Flip to true to help improve Angular + NG_CLI_ANALYTICS = "false"; + # Quieter Terraform logs + TF_IN_AUTOMATION = "true"; + }; + idx = { + # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" + extensions = [ + "Dart-Code.flutter" + "Dart-Code.dart-code" + "hashicorp.terraform" + "ms-vscode.js-debug" + ]; + workspace = { + # Runs when a workspace is first created with this `dev.nix` file + onCreate = { + npm-install = "flutter pub get"; + default.openFiles = [ + "README.md" + "lib/main.dart" + ]; + }; + # To run something each time the workspace is (re)started, use the `onStart` hook + }; + # Enable previews and customize configuration + previews = { + enable = true; + previews = { + web = { + command = [ "flutter" "run" "--machine" "-d" "web-server" "--web-hostname" "0.0.0.0" "--web-port" "$PORT" ]; + manager = "flutter"; + }; + # android = { + # command = [ "flutter" "run" "--machine" "-d" "android" "-d" "localhost:5555" ]; + # manager = "flutter"; + # }; + }; + }; + }; +} diff --git a/firebase_ai_logic_showcase/.idx/integrations.json b/firebase_ai_logic_showcase/.idx/integrations.json new file mode 100644 index 0000000..b9b8a61 --- /dev/null +++ b/firebase_ai_logic_showcase/.idx/integrations.json @@ -0,0 +1,9 @@ +{ + "firebase_hosting": {}, + "cloud_run_deploy": { + "region": "us-central1", + "sourceFlag": "--source services/cloud-run", + "allowUnauthenticatedInvocationsFlag": "--allow-unauthenticated" + }, + "gemini_api": {} + } diff --git a/firebase_ai_logic_showcase/.metadata b/firebase_ai_logic_showcase/.metadata new file mode 100644 index 0000000..05a8ab4 --- /dev/null +++ b/firebase_ai_logic_showcase/.metadata @@ -0,0 +1,45 @@ +# 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: "05db9689081f091050f01aed79f04dce0c750154" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: android + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: ios + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: linux + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: macos + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: web + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: windows + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + + # 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' diff --git a/firebase_ai_logic_showcase/LICENSE.txt b/firebase_ai_logic_showcase/LICENSE.txt new file mode 100644 index 0000000..e58143f --- /dev/null +++ b/firebase_ai_logic_showcase/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/firebase_ai_logic_showcase/README.md b/firebase_ai_logic_showcase/README.md new file mode 100644 index 0000000..29485f4 --- /dev/null +++ b/firebase_ai_logic_showcase/README.md @@ -0,0 +1,103 @@ +# flutter_firebase_ai_sample +**Target Platforms:** iOS, Android, Web + +**Tech Stack:** [Flutter](https://flutter.dev/) (frontend), +[Firebase AI Logic](https://firebase.google.com/docs/ai-logic) + +![Flutter & Firebase AI Sample App Mobile Screenshots](README/flutter_firebase_ai_sample_hero.png) + +This Flutter application demonstrates Firebase AI Logic capabilities through a +series of interactive demos. Firebase AI Logic provides access to the Gemini and +Imagen family of models, enabling developers to build AI-powered experiences in +Flutter apps. + +> [!NOTE] +> Check out this Google I/O 2025 talk for a full walkthrough on Firebase AI Logic: +> [How to build agentic apps with Flutter and Firebase AI Logic](https://www.youtube.com/watch?v=xo271p-Fl_4). + +## Getting Started + +1. Follow [these instructions](https://firebase.google.com/docs/ai-logic/get-started?&api=vertex#set-up-firebase) +to set up a Firebase project & connect the app to Firebase using `flutterfire configure` + +1. Run `flutter pub get` in the root of the project directory `flutter_ai` to +install the Flutter app dependencies + +1. Run `flutter run -d ` to start the app on iOS, Android, or Web. + +> [!TIP] +> Get available devices by running `flutter devices` ex: `AA8A7357`, `macos`, `chrome`. + +`main.dart` is the entry point for the app, but the `lib/flutter_firebase_ai_demo.dart` +file serves as the table of contents for the various demos. It defines the +structure for each demo and presents them in a navigable list on the app's home screen. + +## Explore the interactive demos: + +### Live API +Real-time bidirectional audio and video streaming with Gemini, demonstrating +dynamic and interactive AI communication. +- **Start/End Call:** Tap the "Call" button (phone icon) to initiate or terminate +the real-time audio and video stream with Gemini. +- **Toggle Video:** Once a call is active, tap the "Video" button (camera icon) +to start or stop sending your camera feed. +- **Flip Camera:** If video is active and multiple cameras are available, use +the "Flip Camera" button to switch between them. +- **Mute Audio:** During a call, tap the "Mute" button to toggle your +microphone's audio input. +- **Function Calling:** This demo is integrated with Function Calling, so +you can ask Gemini to use the two tools that are built into the demo: generate +an image or change the color of the app. + +### Multimodal Prompt +Interact with Gemini by asking questions about images, audio, video, or text files, +highlighting the model's ability to process diverse inputs. +- **Select a file:** Tap the "Pick File" button to choose an image, audio, video, +or text file from your device. +- **Enter a prompt:** Type your question or request about the selected file into +the text input field. +- **Ask Gemini:** Tap the "Ask Gemini" button to send the file and your prompt +to the Gemini model. +- **View response:** The response from Gemini will appear in the output display +area. A loading indicator will be shown while Gemini is processing your request. + +### Chat with Nano Banana & Function Calling +Engage in a continuous conversation with Gemini, where the model maintains +conversation history and uses function calling to perform actions or retrieve information. +- **Switch models:** Use the dropdown menu at the top of the screen to switch +between different Gemini models. +- **Type a message:** Enter your message in the input field at the bottom of the screen. +- **Send a message:** Tap the "Send" button to send your message to Gemini. +- **Attach an image (optional):** Tap the "Image" icon to select an image +from your gallery to send with your message. +- **View conversation:** Your messages and Gemini's responses will appear in the chat history. +- **Use tools with function calling:** With `gemini-3.5-flash` (selected by default), +you can ask Gemini to use the two tools that are built into the demo: + - Generate an image using Imagen (e.g., "generate an image of a cat") or + - Change the color of the app (e.g., "change the color to blue"). +- **Nano Banana** With `gemini-3.1-flash-image-preview`, you can generate and edit images. + - **Generate an image:** Enter a text prompt and generate a new image. + - **Edit an image:** Provide instructions for Gemini to edit a previously generated image or select one from your photo library. + +## Implementation +All Firebase AI Logic code has been separated from the Flutter UI code to make +the code easier to read and understand. For each demo, you will find all of the +encapsulated Firebase AI Logic code in their respective `firebase__service.dart` files. +These files can be found in their respective demo directories, with the exception of +the `ImageService` which is shared across demos: Live API and Chat, +so the code is instead located in `lib/shared/firebaseai_imagen_service.dart`. + +Check out [this table](https://firebase.google.com/docs/ai-logic/models) for +more info on Firebase AI Logic's supported models & features. + +## Additional Resources +- [Firebase AI Logic docs](https://firebase.google.com/docs/ai-logic) +- [[Codelab] Build a Gemini powered Flutter app with Flutter & Firebase AI Logic](https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist) + +Feeling inspired? Check out these other Flutter & Firebase AI Logic sample apps! +- [Agentic App Manager](https://github.com/flutter/demos/tree/main/agentic_app_manager): +Build an agentic experience in a Flutter app using Firebase AI Logic. +- [Colorist](https://github.com/flutter/demos/tree/main/vertex_ai_firebase_flutter_app): +Explore LLM tooling interfaces by allowing users to describe colors in natural language. +The app uses Gemini LLM to interpret descriptions and change the color of a +displayed square by calling specialized color tools. diff --git a/firebase_ai_logic_showcase/README/flutter_firebase_ai_sample_hero.png b/firebase_ai_logic_showcase/README/flutter_firebase_ai_sample_hero.png new file mode 100644 index 0000000..00a3763 Binary files /dev/null and b/firebase_ai_logic_showcase/README/flutter_firebase_ai_sample_hero.png differ diff --git a/firebase_ai_logic_showcase/analysis_options.yaml b/firebase_ai_logic_showcase/analysis_options.yaml new file mode 100644 index 0000000..8957fb3 --- /dev/null +++ b/firebase_ai_logic_showcase/analysis_options.yaml @@ -0,0 +1,8 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + prefer_relative_imports: true + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/firebase_ai_logic_showcase/android/.gitignore b/firebase_ai_logic_showcase/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/firebase_ai_logic_showcase/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/firebase_ai_logic_showcase/android/app/build.gradle.kts b/firebase_ai_logic_showcase/android/app/build.gradle.kts new file mode 100644 index 0000000..0281b3e --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.flutter_firebase_ai_sample" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.flutter_firebase_ai_sample" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + ndkVersion = "27.0.12077973" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/firebase_ai_logic_showcase/android/app/src/debug/AndroidManifest.xml b/firebase_ai_logic_showcase/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/AndroidManifest.xml b/firebase_ai_logic_showcase/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dc3eede --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/flutter_firebase_ai_sample/MainActivity.kt b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/flutter_firebase_ai_sample/MainActivity.kt new file mode 100644 index 0000000..8b08e14 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/flutter_firebase_ai_sample/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.flutter_firebase_ai_sample + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt new file mode 100644 index 0000000..8b08e14 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.flutter_firebase_ai_sample + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/drawable-v21/launch_background.xml b/firebase_ai_logic_showcase/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/drawable/launch_background.xml b/firebase_ai_logic_showcase/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/values-night/styles.xml b/firebase_ai_logic_showcase/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/values/styles.xml b/firebase_ai_logic_showcase/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/profile/AndroidManifest.xml b/firebase_ai_logic_showcase/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/firebase_ai_logic_showcase/android/build.gradle.kts b/firebase_ai_logic_showcase/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/firebase_ai_logic_showcase/android/build.gradle.kts @@ -0,0 +1,21 @@ +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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/firebase_ai_logic_showcase/android/gradle.properties b/firebase_ai_logic_showcase/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/firebase_ai_logic_showcase/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/firebase_ai_logic_showcase/android/gradle/wrapper/gradle-wrapper.properties b/firebase_ai_logic_showcase/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afa1e8e --- /dev/null +++ b/firebase_ai_logic_showcase/android/gradle/wrapper/gradle-wrapper.properties @@ -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.10.2-all.zip diff --git a/firebase_ai_logic_showcase/android/settings.gradle.kts b/firebase_ai_logic_showcase/android/settings.gradle.kts new file mode 100644 index 0000000..9e2d35c --- /dev/null +++ b/firebase_ai_logic_showcase/android/settings.gradle.kts @@ -0,0 +1,28 @@ +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.7.0" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/firebase_ai_logic_showcase/assets/firebase-ai-logic.png b/firebase_ai_logic_showcase/assets/firebase-ai-logic.png new file mode 100644 index 0000000..46d73b6 Binary files /dev/null and b/firebase_ai_logic_showcase/assets/firebase-ai-logic.png differ diff --git a/firebase_ai_logic_showcase/assets/gemini-logo.png b/firebase_ai_logic_showcase/assets/gemini-logo.png new file mode 100644 index 0000000..3adcf7c Binary files /dev/null and b/firebase_ai_logic_showcase/assets/gemini-logo.png differ diff --git a/firebase_ai_logic_showcase/configure.sh b/firebase_ai_logic_showcase/configure.sh new file mode 100755 index 0000000..5bab29a --- /dev/null +++ b/firebase_ai_logic_showcase/configure.sh @@ -0,0 +1,159 @@ +#!/bin/bash + +# Check if GOOGLE_CLOUD_PROJECT is defined +if [ -z "${GOOGLE_CLOUD_PROJECT}" ]; then + echo "Error: GOOGLE_CLOUD_PROJECT environment variable is not set." + exit 1 +else + echo "GOOGLE_CLOUD_PROJECT is set to: ${GOOGLE_CLOUD_PROJECT}" +fi + +# Check if CLOUD_SHELL is set to true +if [ "${CLOUD_SHELL}" = "true" ]; then + echo "CLOUD_SHELL is set to true." +else + echo "Error: CLOUD_SHELL environment variable is not set to true." + exit 1 +fi + +# Set the predefined relative application path +APP_PATH="." +echo "APP_PATH is set to: ${APP_PATH}" + +# Define the bootstrap.js path +BOOTSTRAP_JS_PATH="${APP_PATH}/web/bootstrap.js" +echo "BOOTSTRAP_JS_PATH is set to: ${BOOTSTRAP_JS_PATH}" + +# Check if firebase CLI is installed +if ! command -v firebase &> /dev/null +then + echo "Error: firebase command not found. Please install the Firebase CLI." + echo "Installation instructions: https://firebase.google.com/docs/cli" + exit 1 +fi + +# Check if jq is installed +if ! command -v jq &> /dev/null +then + echo "Error: jq command not found. Please install jq to parse JSON." + echo "Installation instructions: https://stedolan.github.io/jq/download/" + exit 1 +fi + +# Enable Web Frameworks experiment +echo "Enabling Firebase Web Frameworks experiment..." +firebase experiments:enable webframeworks + +# Login to Firebase +echo "Logging into Firebase..." +firebase login + +# Check if the firebase login command was successful +if [ $? -ne 0 ]; then + echo "Error: Failed to login to Firebase." + exit 1 +fi + +# Set the default Firebase project +echo "Setting default Firebase project to: ${GOOGLE_CLOUD_PROJECT}" +firebase use ${GOOGLE_CLOUD_PROJECT} --alias default + +# Check if the firebase use command was successful +if [ $? -ne 0 ]; then + echo "Error: Failed to set Firebase project." + exit 1 +fi + +# List Firebase web apps and parse the output +echo "Fetching Firebase web apps list..." +FIREBASE_APPS_JSON=$(firebase --json apps:list WEB) + +# Check if the apps:list command was successful +if [ $? -ne 0 ]; then + echo "Error: Failed to list Firebase apps." + exit 1 +fi + +# Count the number of apps +APP_COUNT=$(echo "${FIREBASE_APPS_JSON}" | jq '.result | length') + +if [ "${APP_COUNT}" -eq 0 ]; then + echo "Error: No WEB apps found in Firebase project ${GOOGLE_CLOUD_PROJECT}." + exit 1 +elif [ "${APP_COUNT}" -gt 1 ]; then + echo "Error: Multiple WEB apps found in Firebase project ${GOOGLE_CLOUD_PROJECT}. This script expects only one." + echo "${FIREBASE_APPS_JSON}" + exit 1 +fi + +# Extract the appId +APP_ID=$(echo "${FIREBASE_APPS_JSON}" | jq -r '.result[0].appId') + +if [ -z "${APP_ID}" ] || [ "${APP_ID}" = "null" ]; then + echo "Error: Could not extract appId from Firebase app list." + echo "${FIREBASE_APPS_JSON}" + exit 1 +fi + +echo "Found single Firebase WEB app with appId: ${APP_ID}" + +# Get the Firebase app config +echo "Fetching Firebase app config for appId: ${APP_ID}" +FIREBASE_CONFIG_JSON=$(firebase --json apps:sdkconfig WEB ${APP_ID}) + +# Check if the apps:sdkconfig command was successful +if [ $? -ne 0 ]; then + echo "Error: Failed to fetch Firebase app config." + exit 1 +fi + +# Extract config values using jq +API_KEY=$(echo "${FIREBASE_CONFIG_JSON}" | jq -r '.result.sdkConfig.apiKey') +AUTH_DOMAIN=$(echo "${FIREBASE_CONFIG_JSON}" | jq -r '.result.sdkConfig.authDomain') +DATABASE_URL=$(echo "${FIREBASE_CONFIG_JSON}" | jq -r '.result.sdkConfig.databaseURL') +PROJECT_ID=$(echo "${FIREBASE_CONFIG_JSON}" | jq -r '.result.sdkConfig.projectId') +STORAGE_BUCKET=$(echo "${FIREBASE_CONFIG_JSON}" | jq -r '.result.sdkConfig.storageBucket') +MESSAGING_SENDER_ID=$(echo "${FIREBASE_CONFIG_JSON}" | jq -r '.result.sdkConfig.messagingSenderId') +MEASUREMENT_ID=$(echo "${FIREBASE_CONFIG_JSON}" | jq -r '.result.sdkConfig.measurementId') + +# Create the directory for bootstrap.js if it doesn't exist +mkdir -p "$(dirname "${BOOTSTRAP_JS_PATH}")" + +# Generate the bootstrap.js file +echo "Generating ${BOOTSTRAP_JS_PATH}..." +cat << EOF > "${BOOTSTRAP_JS_PATH}" +window["APP_TEMPLATE_BOOTSTRAP"] = { + firebase: { + apiKey: "${API_KEY}", + authDomain: "${AUTH_DOMAIN}", + databaseURL: "${DATABASE_URL}", + projectId: "${PROJECT_ID}", + storageBucket: "${STORAGE_BUCKET}", + messagingSenderId: "${MESSAGING_SENDER_ID}", + appId: "${APP_ID}", + measurementId: "${MEASUREMENT_ID}", + }, +}; +EOF + +if [ $? -ne 0 ]; then + echo "Error: Failed to generate ${BOOTSTRAP_JS_PATH}." + exit 1 +fi + +echo "Successfully generated ${BOOTSTRAP_JS_PATH}." +echo "Configuration checks and setup passed." + +echo "Making sure we can run flutter" +git config --global --add safe.directory /google/flutter + +(cd "${APP_PATH}" && flutter) + +if [ $? -ne 0 ]; then + echo "Error: Failed to run flutter CLI." + exit 1 +fi + +echo "Successfully ran flutter CLI." +exit 0 + diff --git a/firebase_ai_logic_showcase/deploy.sh b/firebase_ai_logic_showcase/deploy.sh new file mode 100755 index 0000000..81013c6 --- /dev/null +++ b/firebase_ai_logic_showcase/deploy.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Set the predefined relative application path +APP_PATH="." +echo "APP_PATH is set to: ${APP_PATH}" + +# Define the bootstrap.js path +BOOTSTRAP_JS_PATH="${APP_PATH}/web/bootstrap.js" +echo "BOOTSTRAP_JS_PATH is set to: ${BOOTSTRAP_JS_PATH}" + +# Check if bootstrap.js exists +if [ ! -f "${BOOTSTRAP_JS_PATH}" ]; then + echo "Error: ${BOOTSTRAP_JS_PATH} not found. Please run configure.sh first." + exit 1 +fi + +echo "${BOOTSTRAP_JS_PATH} found." + +echo "Cleanup the build..." +(cd "${APP_PATH}" && flutter clean) + +# Build the Flutter application +echo "Building the application..." +(cd "${APP_PATH}" && flutter build web --release) + +# Deploy to Firebase Hosting +echo "Deploying to Firebase Hosting..." +(cd "${APP_PATH}" && firebase deploy --only=hosting) diff --git a/firebase_ai_logic_showcase/firebase.json b/firebase_ai_logic_showcase/firebase.json new file mode 100644 index 0000000..2b2992c --- /dev/null +++ b/firebase_ai_logic_showcase/firebase.json @@ -0,0 +1,5 @@ +{ + "hosting": { + "public": "build/web" + } +} diff --git a/firebase_ai_logic_showcase/idx-template.json b/firebase_ai_logic_showcase/idx-template.json new file mode 100644 index 0000000..0406147 --- /dev/null +++ b/firebase_ai_logic_showcase/idx-template.json @@ -0,0 +1,28 @@ +{ + "name": "Flutter app with Firebase AI logic", + "description": "A sample flutter app that demonstrates how to use the Gemini API with Firebase AI Logic", + "categories": [ + "AI & ML", + "Web", + "Firebase", + "Flutter", + "Android", + "iOS" + ], + "icon_image_url": "https://www.gstatic.com/monospace/240513/logo_firebase.svg", + "publisher": "Google LLC", + "params": [ + { + "id": "projectId", + "name": "Firebase Project ID", + "type": "string", + "required": true + }, + { + "id": "bootstrapJs", + "name": "Sample App Bootstrap", + "type": "string", + "required": false + } + ] +} \ No newline at end of file diff --git a/firebase_ai_logic_showcase/idx-template.nix b/firebase_ai_logic_showcase/idx-template.nix new file mode 100644 index 0000000..febda87 --- /dev/null +++ b/firebase_ai_logic_showcase/idx-template.nix @@ -0,0 +1,20 @@ +{ pkgs, projectId, bootstrapJs, ... }: +{ + bootstrap = '' + cp -rf ${./.} "$out/" + chmod -R +w "$out" + echo 'bootstrapJs was set to: ${bootstrapJs}' + # Apply project ID to configs + if [ -z '${bootstrapJs}' ] || [ '${bootstrapJs}' = 'false' ] + then + sed -e 's//${projectId}/' ${.idx/dev.nix} > "$out/.idx/dev.nix" + else + sed -e 's//${projectId}/' ${.idx/dev.nix} | sed -e 's/terraform init/# terraform init/' | sed -e 's/terraform apply/# terraform apply/' > "$out/.idx/dev.nix" + echo '${bootstrapJs}' > "$out/web/bootstrap.js" + echo '{"projects":{"default":"${projectId}"}}' > "$out/.firebaserc" + fi + # Remove the template files themselves and any connection to the template's + # Git repository + rm -rf "$out/.git" "$out/idx-template".{nix,json} "$out/node_modules" + ''; +} \ No newline at end of file diff --git a/firebase_ai_logic_showcase/ios/.gitignore b/firebase_ai_logic_showcase/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +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 diff --git a/firebase_ai_logic_showcase/ios/Flutter/AppFrameworkInfo.plist b/firebase_ai_logic_showcase/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/firebase_ai_logic_showcase/ios/Flutter/Debug.xcconfig b/firebase_ai_logic_showcase/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/firebase_ai_logic_showcase/ios/Flutter/Release.xcconfig b/firebase_ai_logic_showcase/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/firebase_ai_logic_showcase/ios/Podfile b/firebase_ai_logic_showcase/ios/Podfile new file mode 100644 index 0000000..236ec2d --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '15.0' #FlutterFire plugins require minimum of iOS 15 https://firebase.google.com/docs/flutter/setup?platform=ios + +# 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 diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.pbxproj b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0f11db6 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,753 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2E93BD09C0B0FFDB52311EAA /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3760F5D70F4BD5A3F3B08C19 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + EF7DAC43BEEDC80B2E3D9979 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */; }; +/* 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 */ + 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2B33F176EE317BBD897D8C8E /* 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 = ""; }; + 2D957529EFAEE0BDD3A76F18 /* 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8E6CD9C6541A3CF46C64E1C9 /* 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 = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + BA6D30E40E8F68B06E56229C /* 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 = ""; }; + E9425AF2FD1A44E278C01648 /* 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 = ""; }; + F9CFD1A5C79D4AABA9C9A108 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 37FD498E274777199498BC21 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2E93BD09C0B0FFDB52311EAA /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3760F5D70F4BD5A3F3B08C19 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3044D5D4F7C58D0A98D41E15 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */, + 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 6B0AAB17435D3F0DE252CF55 /* Pods */ = { + isa = PBXGroup; + children = ( + BA6D30E40E8F68B06E56229C /* Pods-Runner.debug.xcconfig */, + F9CFD1A5C79D4AABA9C9A108 /* Pods-Runner.release.xcconfig */, + 8E6CD9C6541A3CF46C64E1C9 /* Pods-Runner.profile.xcconfig */, + 2B33F176EE317BBD897D8C8E /* Pods-RunnerTests.debug.xcconfig */, + E9425AF2FD1A44E278C01648 /* Pods-RunnerTests.release.xcconfig */, + 2D957529EFAEE0BDD3A76F18 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 6B0AAB17435D3F0DE252CF55 /* Pods */, + 3044D5D4F7C58D0A98D41E15 /* Frameworks */, + 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 04715B1AF939C799C4D37CEC /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 37FD498E274777199498BC21 /* 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 = ( + E56B7B1A5FD7695313B2FE04 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E65799E83B05311337559E13 /* [CP] Embed Pods Frameworks */, + F554392F020C8FA33594A554 /* [CP] Copy Pods Resources */, + ); + 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 = ( + ); + 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 */, + EF7DAC43BEEDC80B2E3D9979 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 04715B1AF939C799C4D37CEC /* [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"; + }; + E56B7B1A5FD7695313B2FE04 /* [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; + }; + E65799E83B05311337559E13 /* [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; + }; + F554392F020C8FA33594A554 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.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 */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + 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 = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2B33F176EE317BBD897D8C8E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E9425AF2FD1A44E278C01648 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D957529EFAEE0BDD3A76F18 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + 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; + "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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + 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 = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */; +} diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcworkspace/contents.xcworkspacedata b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/firebase_ai_logic_showcase/ios/Runner/AppDelegate.swift b/firebase_ai_logic_showcase/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/firebase_ai_logic_showcase/ios/Runner/Base.lproj/LaunchScreen.storyboard b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner/Base.lproj/Main.storyboard b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner/Info.plist b/firebase_ai_logic_showcase/ios/Runner/Info.plist new file mode 100644 index 0000000..31531a5 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Multimodal Ai Prototype + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_firebase_ai_sample + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSMicrophoneUsageDescription + Need to record user voice to communicate with Gemini + NSCameraUsageDescription + Need camera to show Gemini live video stream + NSPhotoLibraryUsageDescription + Need pick images in chat + + diff --git a/firebase_ai_logic_showcase/ios/Runner/Runner-Bridging-Header.h b/firebase_ai_logic_showcase/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/firebase_ai_logic_showcase/ios/RunnerTests/RunnerTests.swift b/firebase_ai_logic_showcase/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/firebase_ai_logic_showcase/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart b/firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart new file mode 100644 index 0000000..f6b63e2 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart @@ -0,0 +1,198 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:permission_handler/permission_handler.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import '../../shared/ui/app_frame.dart'; +import '../../shared/ui/app_spacing.dart'; +import '../../shared/ui/chat_components/ui_components.dart'; +import '../../shared/chat_service.dart'; +import '../../shared/models/models.dart'; + +class ChatDemo extends ConsumerStatefulWidget { + const ChatDemo({super.key}); + + @override + ConsumerState createState() => _ChatDemoState(); +} + +class _ChatDemoState extends ConsumerState { + // Service for interacting with the Gemini API. + late final ChatService _chatService; + + // UI State + final List _messages = []; + final TextEditingController _userTextInputController = + TextEditingController(); + Uint8List? _attachment; + final ScrollController _scrollController = ScrollController(); + bool _loading = false; + + @override + void initState() { + super.initState(); + final model = geminiModels.selectModel('gemini-3.5-flash'); + _chatService = ChatService(ref, model); + _chatService.init(); + _userTextInputController.text = + 'Hey Gemini! Can you set the app color to purple?'; + } + + @override + void didChangeDependencies() { + requestPermissions(); + super.didChangeDependencies(); + } + + @override + void dispose() { + _scrollController.dispose(); + _userTextInputController.dispose(); + super.dispose(); + } + + Future requestPermissions() async { + if (!kIsWeb) { + await Permission.manageExternalStorage.request(); + } + } + + void _scrollToEnd() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + void _pickImage() async { + final pickedImage = await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + + if (pickedImage != null) { + final imageBytes = await pickedImage.readAsBytes(); + setState(() { + _attachment = imageBytes; + }); + log('attachment saved!'); + } + } + + void sendMessage(String text) async { + if (text.isEmpty) return; + + setState(() { + _loading = true; + }); + + // Add user message to UI + final userMessageText = text.trim(); + final userAttachment = _attachment; + _messages.add( + MessageData( + text: userMessageText, + image: userAttachment != null ? Image.memory(userAttachment) : null, + fromUser: true, + ), + ); + setState(() { + _attachment = null; + _userTextInputController.clear(); + }); + _scrollToEnd(); + + // Construct the Content object for the service + final content = (userAttachment != null) + ? Content.multi([ + TextPart(userMessageText), + InlineDataPart('image/jpeg', userAttachment), + ]) + : Content.text(userMessageText); + + // Call the service and handle the response + try { + final chatResponse = await _chatService.sendMessage(content); + _messages.add( + MessageData( + text: chatResponse.text, + image: chatResponse.image, + fromUser: false, + ), + ); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toString()), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } finally { + setState(() { + _loading = false; + }); + _scrollToEnd(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: AppFrame( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: MessageListView( + messages: _messages, + scrollController: _scrollController, + ), + ), + if (_loading) const LinearProgressIndicator(), + AttachmentPreview(attachment: _attachment), + ], + ), + ), + ), + ), + ), + ], + ), + bottomNavigationBar: MessageInputBar( + textController: _userTextInputController, + loading: _loading, + sendMessage: sendMessage, + onPickImagePressed: _pickImage, + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat_nano/chat_nano_demo.dart b/firebase_ai_logic_showcase/lib/demos/chat_nano/chat_nano_demo.dart new file mode 100644 index 0000000..1e6b06a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat_nano/chat_nano_demo.dart @@ -0,0 +1,234 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:permission_handler/permission_handler.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import '../../shared/ui/app_frame.dart'; +import '../../shared/ui/app_spacing.dart'; +import '../../shared/ui/blaze_warning.dart'; +import '../../shared/ui/chat_components/ui_components.dart'; +import '../../shared/chat_service.dart'; +import '../../shared/models/models.dart'; + +class ChatDemoNano extends ConsumerStatefulWidget { + const ChatDemoNano({super.key}); + + @override + ConsumerState createState() => ChatDemoNanoState(); +} + +class ChatDemoNanoState extends ConsumerState { + // Service for interacting with the Gemini API. + late final ChatService _chatService; + + // UI State + final List _messages = []; + final TextEditingController _userTextInputController = + TextEditingController(); + Uint8List? _attachment; + final ScrollController _scrollController = ScrollController(); + bool _loading = false; + OverlayPortalController opController = OverlayPortalController(); + static bool _warningHasBeenShown = false; + + @override + void initState() { + super.initState(); + _chatService = ChatService(ref); + geminiModels.selectModel('gemini-3-pro-image-preview'); + _chatService.init(); + _userTextInputController.text = + 'Hot air balloons rising over the San Francisco Bay at golden hour with a view of the Golden Gate Bridge. Make it anime style.'; + _checkAndShowBlazeWarning(); + } + + void _checkAndShowBlazeWarning() { + if (!_warningHasBeenShown) { + _warningHasBeenShown = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + showBlazeWarning(); + } + }); + } + } + + @override + void didChangeDependencies() { + requestPermissions(); + super.didChangeDependencies(); + } + + @override + void dispose() { + _scrollController.dispose(); + _userTextInputController.dispose(); + super.dispose(); + } + + Future requestPermissions() async { + if (!kIsWeb) { + await Permission.manageExternalStorage.request(); + } + } + + void _scrollToEnd() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + void _pickImage() async { + final pickedImage = await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + + if (pickedImage != null) { + final imageBytes = await pickedImage.readAsBytes(); + setState(() { + _attachment = imageBytes; + }); + log('attachment saved!'); + } + } + + void sendMessage(String text) async { + if (text.isEmpty) return; + + setState(() { + _loading = true; + }); + + // Add user message to UI + final userMessageText = text.trim(); + final userAttachment = _attachment; + _messages.add( + MessageData( + text: userMessageText, + image: userAttachment != null ? Image.memory(userAttachment) : null, + fromUser: true, + ), + ); + setState(() { + _attachment = null; + _userTextInputController.clear(); + }); + _scrollToEnd(); + + // Construct the Content object for the service + final content = (userAttachment != null) + ? Content.multi([ + TextPart(userMessageText), + InlineDataPart('image/jpeg', userAttachment), + ]) + : Content.text(userMessageText); + + // Call the service and handle the response + try { + final chatResponse = await _chatService.sendMessage(content); + _messages.add( + MessageData( + text: chatResponse.text, + image: chatResponse.image, + fromUser: false, + ), + ); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toString()), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } finally { + setState(() { + _loading = false; + }); + _scrollToEnd(); + } + } + + void showBlazeWarning() { + showDialog( + context: context, + builder: (context) { + return Dialog( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 600), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [BlazeWarning()], + ), + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: AppFrame( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: MessageListView( + messages: _messages, + scrollController: _scrollController, + ), + ), + if (_loading) const LinearProgressIndicator(), + AttachmentPreview(attachment: _attachment), + ], + ), + ), + ), + ), + ), + ], + ), + bottomNavigationBar: MessageInputBar( + textController: _userTextInputController, + loading: _loading, + sendMessage: sendMessage, + onPickImagePressed: _pickImage, + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/firebaseai_live_api_service.dart b/firebase_ai_logic_showcase/lib/demos/live_api/firebaseai_live_api_service.dart new file mode 100644 index 0000000..3553407 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/firebaseai_live_api_service.dart @@ -0,0 +1,225 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../shared/app_state.dart'; +import '../../shared/firebaseai_imagen_service.dart'; +import '../../shared/function_calling/tools.dart'; +import 'utilities/audio_output.dart'; + +/// A service that handles all communication with the Firebase AI Gemini Live API. +/// +/// This service demonstrates how to use the `liveGenerativeModel()` to create +/// a real-time, bidirectional audio & video stream with Gemini. It manages the +/// `LiveSession` and processes streaming responses, including tool calls. +/// +/// For more information, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/live-api?api=dev +class LiveApiService { + final AudioOutput _audioOutput; + final WidgetRef _ref; + // Callbacks for UI updates handled by setState + final void Function(bool isLoading) onImageLoadingChange; + final void Function(Uint8List imageBytes) onImageGenerated; + final void Function(String error) onError; + + LiveApiService({ + required AudioOutput audioOutput, + required WidgetRef ref, + required this.onImageLoadingChange, + required this.onImageGenerated, + required this.onError, + }) : _audioOutput = audioOutput, + _ref = ref; + + final LiveGenerativeModel + _liveModel = FirebaseAI.googleAI().liveGenerativeModel( + systemInstruction: Content.text( + 'You are a helpful assistant. If you have a tool to help the user, please use it.', + ), + model: 'gemini-2.5-flash-native-audio-preview-12-2025', + liveGenerationConfig: LiveGenerationConfig( + speechConfig: SpeechConfig(voiceName: 'fenrir'), + responseModalities: [ResponseModalities.audio], + ), + tools: [ + Tool.functionDeclarations([ + setAppColorTool, + // Gemini Flash Image currently requires the pay-as-you-go Blaze plan. + generateImageTool, + ]), + ], + ); + + late LiveSession _session; + bool _liveSessionIsOpen = false; + + Future connect() async { + if (_liveSessionIsOpen) return; + try { + _session = await _liveModel.connect(); + _liveSessionIsOpen = true; + unawaited(processMessagesContinuously()); + } catch (e) { + log('Error connecting to live session: $e'); + onError('Failed to start the call. Please try again.'); + } + } + + Future close() async { + if (!_liveSessionIsOpen) return; + try { + await _session.close(); + } catch (e) { + log('Error closing live session: $e'); + // Don't necessarily need to show an error to the user on close. + } finally { + _liveSessionIsOpen = false; + } + } + + bool get isSessionOpen => _liveSessionIsOpen; + + void sendMediaStream(Stream stream) { + if (!_liveSessionIsOpen) return; + _session.sendMediaStream(stream); + } + + Future processMessagesContinuously() async { + try { + await for (final response in _session.receive()) { + LiveServerMessage message = response.message; + await _handleLiveServerMessage(message); + } + log('Live session receive stream completed.'); + } catch (e) { + log('Error receiving live session messages: $e'); + onError('Something went wrong during the call. Please try again.'); + } + } + + Future _handleLiveServerMessage(LiveServerMessage response) async { + if (response is LiveServerContent) { + if (response.modelTurn != null) { + await _handleLiveServerContent(response); + } + if (response.turnComplete != null && response.turnComplete!) { + await _handleTurnComplete(); + } + if (response.interrupted != null && response.interrupted!) { + log('Interrupted: $response'); + } + } + + if (response is LiveServerToolCall && response.functionCalls != null) { + await _handleLiveServerToolCall(response); + } + } + + Future _handleLiveServerContent(LiveServerContent response) async { + final partList = response.modelTurn?.parts; + if (partList != null) { + for (final part in partList) { + switch (part) { + case TextPart textPart: + await _handleTextPart(textPart); + case InlineDataPart inlineDataPart: + await _handleInlineDataPart(inlineDataPart); + default: + log('Received part with type ${part.runtimeType}'); + } + } + } + } + + Future _handleInlineDataPart(InlineDataPart part) async { + if (part.mimeType.startsWith('audio')) { + _audioOutput.addDataToAudioStream(part.bytes); + } + } + + Future _handleTextPart(TextPart part) async { + log('Text message from Gemini: ${part.text}'); + } + + Future _handleTurnComplete() async { + log('Model is done generating. Turn complete!'); + final halfSecondOfSilence = Uint8List(24000); + _audioOutput.addDataToAudioStream(halfSecondOfSilence); + } + + Future _handleLiveServerToolCall(LiveServerToolCall response) async { + var functionCalls = response.functionCalls; + if (functionCalls == null || functionCalls.isEmpty) return; + + // The API currently only supports one function call per turn. + var functionCall = functionCalls.first; + log("Gemini made a function call: ${functionCall.name}"); + + switch (functionCall.name) { + case 'GenerateImage': + await _handleGenerateImage(functionCall); + break; + case 'SetAppColor': + _handleSetAppColor(functionCall); + break; + default: + log('Unknown function call: ${functionCall.name}'); + } + } + + Future _handleGenerateImage(FunctionCall functionCall) async { + onImageLoadingChange(true); + try { + final imageDescription = functionCall.args['description']?.toString(); + if (imageDescription == null) { + onError('Image generation failed: No description provided.'); + return; + } + final image = await ImageGenerationService().generateImage( + imageDescription, + ); + onImageGenerated(image); + } catch (e) { + log('Error generating image: $e'); + onError('Sorry, the image could not be generated.'); + } finally { + onImageLoadingChange(false); + } + } + + void _handleSetAppColor(FunctionCall functionCall) { + try { + final red = functionCall.args['red']! as int; + final green = functionCall.args['green']! as int; + final blue = functionCall.args['blue']! as int; + final newSeedColor = Color.fromRGBO(red, green, blue, 1); + _ref.read(appStateProvider).setAppColor(newSeedColor); + } catch (e) { + log('Error setting app color from tool call: $e'); + onError('Sorry, there was an error applying the color.'); + } + } + + void dispose() { + if (_liveSessionIsOpen) { + unawaited(close()); + } + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/live_api_demo.dart b/firebase_ai_logic_showcase/lib/demos/live_api/live_api_demo.dart new file mode 100644 index 0000000..195f57e --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/live_api_demo.dart @@ -0,0 +1,297 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'ui_components/ui_components.dart'; +import 'utilities/utilities.dart'; +import 'firebaseai_live_api_service.dart'; + +class LiveAPIDemo extends ConsumerStatefulWidget { + const LiveAPIDemo({super.key}); + + @override + ConsumerState createState() => _LiveAPIDemoState(); +} + +/// The main state for the Live API demo. +/// +/// This stateful widget orchestrates the UI and manages the state for the demo, +/// including handling user input, managing the call lifecycle, and coordinating +/// with the [LiveApiService] and I/O utilities. +class _LiveAPIDemoState extends ConsumerState { + // Service for interacting with the Gemini API via Firebase AI. + late LiveApiService _liveApiService; + + // Utilities for handling device I/O. + late final AudioInput _audioInput = AudioInput(); + late final AudioOutput _audioOutput = AudioOutput(); + late final VideoInput _videoInput = VideoInput(); + + // Initialization flags. + bool _videoIsInitialized = false; + + // UI State flags. + bool _isConnecting = false; // True when setting up the Gemini session. + bool _isCallActive = false; // True when the audio stream is active. + bool _cameraIsActive = false; // True when sending video to Gemini. + bool _loadingImage = false; // True when waiting for an image to be generated. + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkAndInitializeIO(); + }); + } + + @override + void didUpdateWidget(LiveAPIDemo oldWidget) { + super.didUpdateWidget(oldWidget); + _checkAndInitializeIO(); + } + + Future _checkAndInitializeIO() async { + await _initializeAudio(); + await _initializeVideo(); + _liveApiService = LiveApiService( + audioOutput: _audioOutput, + ref: ref, // Pass the ref to the service + onImageLoadingChange: _onImageLoadingChange, + onImageGenerated: _onImageGenerated, + onError: _showErrorSnackBar, + ); + } + + @override + void dispose() { + _audioInput.dispose(); + _audioOutput.dispose(); + _videoInput.dispose(); + _liveApiService.dispose(); + super.dispose(); + } + + void _showErrorSnackBar(String message) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + } + + //================================================================================ + // UI Callbacks + //================================================================================ + + void _onImageLoadingChange(bool isLoading) { + setState(() { + _loadingImage = isLoading; + }); + } + + void _onImageGenerated(Uint8List imageBytes) { + if (!mounted) return; + showDialog( + context: context, + builder: (context) { + return GeneratedImageDialog(imageBytes: imageBytes); + }, + ); + } + + //================================================================================ + // Call Lifecycle + //================================================================================ + + void toggleCall() async { + _isCallActive ? await stopCall() : await startCall(); + } + + Future startCall() async { + // Initialize the camera controller here to ensure it's fresh for each call. + // This prevents a bug where the camera preview freezes on subsequent calls. + if (_videoIsInitialized) { + await _videoInput.initializeCameraController(); + } + + setState(() { + _isConnecting = true; + }); + + await _liveApiService.connect(); + + setState(() { + _isConnecting = false; + }); + + var audioInputStream = await _audioInput.startRecordingStream(); + log('Audio input stream is recording!'); + + await _audioOutput.playStream(); + log('Audio output stream is playing!'); + + setState(() { + _isCallActive = true; + }); + + _liveApiService.sendMediaStream( + audioInputStream.map((data) { + return InlineDataPart('audio/pcm', data); + }), + ); + } + + Future stopCall() async { + if (_cameraIsActive) { + stopVideoStream(); + } + await _audioInput.stopRecording(); + await _audioOutput.stopStream(); + + setState(() { + _isConnecting = true; + }); + + await _liveApiService.close(); + + setState(() { + _isConnecting = false; + _isCallActive = false; + }); + } + + //================================================================================ + // I/O Initialization and Control + //================================================================================ + + Future _initializeAudio() async { + try { + await _audioInput.init(); // Initialize Audio Input + await _audioOutput.init(); // Initialize Audio Output + } catch (e) { + log("Error during audio initialization: $e"); + if (!mounted) return; + + var errorSnackBar = SnackBar( + content: const Text('Oops! Something went wrong with the audio setup.'), + action: SnackBarAction(label: 'Retry', onPressed: _initializeAudio), + ); + ScaffoldMessenger.of(context).showSnackBar(errorSnackBar); + } + } + + Future _initializeVideo() async { + try { + await _videoInput.init(); + setState(() { + _videoIsInitialized = true; + }); + } catch (e) { + log("Error during video initialization: $e"); + } + } + + void startVideoStream() { + if (!_videoIsInitialized || !_isCallActive || _cameraIsActive) { + return; + } + + Stream imageStream = _videoInput.startStreamingImages(); + + _liveApiService.sendMediaStream( + imageStream.map((data) { + return InlineDataPart("image/jpeg", data); + }), + ); + + setState(() { + _cameraIsActive = true; + }); + } + + void stopVideoStream() async { + await _videoInput.stopStreamingImages(); + setState(() { + _cameraIsActive = false; + }); + } + + void toggleVideoStream() async { + _cameraIsActive ? stopVideoStream() : startVideoStream(); + } + + Future toggleMuteInput() async { + await _audioInput.togglePauseRecording(); + setState(() {}); // Rebuild mute button icon + } + + //================================================================================ + // Build Method + //================================================================================ + + @override + Widget build(BuildContext context) { + final audioInput = _audioInput; + final videoInput = _videoInput; + + return ListenableBuilder( + listenable: audioInput, + builder: (context, child) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: Column( + children: [ + Expanded( + child: LiveApiBody( + cameraIsActive: _cameraIsActive, + cameraController: videoInput.controllerInitialized + ? videoInput.cameraController + : null, + settingUpLiveSession: _isConnecting, + loadingImage: _loadingImage, + ), + ), + BottomBar( + children: [ + FlipCameraButton( + onPressed: _cameraIsActive && videoInput.cameras.length > 1 + ? videoInput.flipCamera + : null, + ), + VideoButton( + isActive: _cameraIsActive, + onPressed: toggleVideoStream, + ), + AudioVisualizer( + audioStreamIsActive: _isCallActive, + amplitudeStream: audioInput.amplitudeStream, + ), + MuteButton( + isMuted: audioInput.isPaused, + onPressed: _isCallActive ? toggleMuteInput : null, + ), + CallButton(isActive: _isCallActive, onPressed: toggleCall), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/audio_visualizer.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/audio_visualizer.dart new file mode 100644 index 0000000..bbb1bd8 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/audio_visualizer.dart @@ -0,0 +1,40 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:waveform_flutter/waveform_flutter.dart'; +import 'sound_waves.dart'; + +class AudioVisualizer extends StatelessWidget { + const AudioVisualizer({ + super.key, + required this.audioStreamIsActive, + this.amplitudeStream, + }); + + final bool audioStreamIsActive; + final Stream? amplitudeStream; + + @override + Widget build(BuildContext context) { + return (audioStreamIsActive && amplitudeStream != null) + ? Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Soundwaves(amplitudeStream: amplitudeStream!), + ), + ) + : const Spacer(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/bottom_bar.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/bottom_bar.dart new file mode 100644 index 0000000..3d363ce --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/bottom_bar.dart @@ -0,0 +1,146 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class BottomBar extends StatelessWidget { + const BottomBar({required this.children, super.key}); + + final List children; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainer, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Row(children: children), + ), + ), + ); + } +} + +class FlipCameraButton extends StatelessWidget { + const FlipCameraButton({this.onPressed, super.key}); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + onPressed: onPressed, + icon: const Padding( + padding: EdgeInsets.all(AppSpacing.s4), + child: Icon(Icons.flip_camera_ios_outlined), + ), + ), + ); + } +} + +class VideoButton extends StatelessWidget { + const VideoButton({required this.isActive, this.onPressed, super.key}); + + final bool isActive; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + style: isActive + ? ButtonStyle( + backgroundColor: WidgetStateProperty.all( + const Color.fromARGB(240, 238, 255, 244), + ), + iconColor: WidgetStateProperty.all(Colors.black87), + ) + : const ButtonStyle(backgroundColor: null), + onPressed: onPressed, + icon: const Padding( + padding: EdgeInsets.all(AppSpacing.s4), + child: Icon(Icons.video_call_rounded), + ), + ), + ); + } +} + +class MuteButton extends StatelessWidget { + const MuteButton({required this.isMuted, this.onPressed, super.key}); + + final bool isMuted; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isMuted ? null : const Color.fromARGB(240, 238, 255, 244), + ), + ), + onPressed: onPressed, + icon: Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: isMuted + ? const Icon(Icons.mic_off) + : const Icon(color: Colors.black87, Icons.mic_none), + ), + ), + ); + } +} + +class CallButton extends StatelessWidget { + const CallButton({required this.isActive, this.onPressed, super.key}); + + final bool isActive; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isActive + ? const Color.fromARGB(255, 199, 39, 27) + : Colors.green[500], + ), + ), + onPressed: onPressed, + icon: Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: Icon(isActive ? Icons.phone_disabled_outlined : Icons.phone), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/camera_previews.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/camera_previews.dart new file mode 100644 index 0000000..9715c65 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/camera_previews.dart @@ -0,0 +1,91 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:camera/camera.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class SquareCameraPreview extends StatelessWidget { + const SquareCameraPreview({required this.controller, super.key}); + + final CameraController controller; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 352.0, // Adjusted from 350 to be a multiple of 4 + height: 352.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + child: AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(AppSpacing.s16), + ), + // The camera preview is often not a square. To fill the 1:1 aspect + // ratio, we scale the preview to cover the area and clip it. + child: Transform.scale( + scale: controller.value.aspectRatio / 1, + child: Center(child: CameraPreview(controller)), + ), + ), + ), + ), + ); + } +} + +class FullCameraPreview extends StatefulWidget { + const FullCameraPreview({required this.controller, super.key}); + + final CameraController controller; + + @override + State createState() => _FullCameraPreviewState(); +} + +class _FullCameraPreviewState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animController; + + @override + void initState() { + super.initState(); + _animController = AnimationController( + vsync: this, // the SingleTickerProviderStateMixin + duration: const Duration(seconds: 1), + ); + } + + @override + void dispose() { + super.dispose(); + _animController.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(AppSpacing.s16)), + child: CameraPreview(widget.controller), + ), + ).animate(controller: _animController).scaleXY().fadeIn(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/generated_image_dialog.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/generated_image_dialog.dart new file mode 100644 index 0000000..9592813 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/generated_image_dialog.dart @@ -0,0 +1,52 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:flutter/material.dart'; + +class GeneratedImageDialog extends StatelessWidget { + const GeneratedImageDialog({super.key, required this.imageBytes}); + + final Uint8List imageBytes; + + @override + Widget build(BuildContext context) { + return Dialog( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Center(child: Image.memory(imageBytes)), + ), + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: IconButton.filled( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_api_body.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_api_body.dart new file mode 100644 index 0000000..7e1dd58 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_api_body.dart @@ -0,0 +1,62 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import 'camera_previews.dart'; +import 'sound_waves.dart'; + +class LiveApiBody extends StatelessWidget { + const LiveApiBody({ + super.key, + required this.cameraIsActive, + this.cameraController, + required this.settingUpLiveSession, + required this.loadingImage, + }); + + final bool cameraIsActive; + final CameraController? cameraController; + final bool settingUpLiveSession; + final bool loadingImage; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: cameraIsActive && cameraController != null + ? Center(child: FullCameraPreview(controller: cameraController!)) + : CenterCircle( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s60), + child: settingUpLiveSession + ? const CircularProgressIndicator() + : Image.asset('assets/gemini-logo.png'), + ), + ), + ), + if (loadingImage) + const Column( + children: [ + Text('Beep. Boop. Bop. Generating your image...'), + SizedBox.square(dimension: AppSpacing.s8), + LinearProgressIndicator(semanticsLabel: 'Generating image...'), + ], + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_demo_app_bar.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_demo_app_bar.dart new file mode 100644 index 0000000..4cf3bdb --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_demo_app_bar.dart @@ -0,0 +1,30 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import 'package:flutter/material.dart'; + +class LiveApiDemoAppBar extends StatelessWidget implements PreferredSizeWidget { + const LiveApiDemoAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: Colors.transparent, + leadingWidth: 100, + title: const Text('Gemini Live API Demo'), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} \ No newline at end of file diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/sound_waves.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/sound_waves.dart new file mode 100644 index 0000000..a976380 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/sound_waves.dart @@ -0,0 +1,120 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:waveform_flutter/waveform_flutter.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class CenterCircle extends StatelessWidget { + const CenterCircle({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Center( + child: CustomPaint( + size: const Size(160, 160), + painter: NestedCirclesPainter( + color: Theme.of(context).colorScheme.primary, + strokeWidth: 1.0, + gapBetweenCircles: 4.0, + ), + child: child, + ), + ); + } +} + +class Soundwaves extends StatelessWidget { + const Soundwaves({required this.amplitudeStream, super.key}); + + final Stream amplitudeStream; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.s16), + child: SizedBox( + height: 48, + child: AnimatedWaveList( + stream: amplitudeStream, + barBuilder: (animation, amplitude) { + return WaveFormBar( + amplitude: amplitude, + animation: animation, + color: Theme.of(context).colorScheme.primary, + ); + }, + ), + ), + ); + } +} + +// Custom Painter for drawing two nested circles +class NestedCirclesPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double gapBetweenCircles; // The space between the two circles + + NestedCirclesPainter({ + this.color = Colors.white54, // Default color for the circles + this.strokeWidth = 1.5, // Default stroke width for both circles + this.gapBetweenCircles = 4.0, // Default gap between the circles + }); + + @override + void paint(Canvas canvas, Size size) { + // Calculate the center of the drawing area + final Offset center = Offset(size.width / 2, size.height / 2); + + // Configure the paint properties (same for both circles) + final Paint paint = Paint() + ..color = color + .withValues(alpha: 0.7) // Make circles slightly transparent + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; // Draw the outline + + // Calculate the radius for the outer circle + // Ensure it fits within the bounds defined by 'size' + final double outerRadius = + min(size.width / 2, size.height / 2) - strokeWidth / 2; + + // Calculate the radius for the inner circle + final double innerRadius = + outerRadius - gapBetweenCircles - strokeWidth / 2; + + // Ensure inner radius is not negative + if (innerRadius > 0) { + // Draw the outer circle + canvas.drawCircle(center, outerRadius, paint); + // Draw the inner circle + canvas.drawCircle(center, innerRadius, paint); + } else { + // If the gap is too large, just draw the outer circle + canvas.drawCircle(center, outerRadius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + // Repaint only if properties change + return oldDelegate is NestedCirclesPainter && + (oldDelegate.color != color || + oldDelegate.strokeWidth != strokeWidth || + oldDelegate.gapBetweenCircles != gapBetweenCircles); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/ui_components.dart new file mode 100644 index 0000000..d53ca4a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/ui_components.dart @@ -0,0 +1,21 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'bottom_bar.dart'; +export 'camera_previews.dart'; +export 'sound_waves.dart'; +export 'live_api_body.dart'; +export 'audio_visualizer.dart'; +export 'live_demo_app_bar.dart'; +export 'generated_image_dialog.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_input.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_input.dart new file mode 100644 index 0000000..1747c5e --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_input.dart @@ -0,0 +1,118 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:record/record.dart'; +import 'package:waveform_flutter/waveform_flutter.dart' as wf; + +class AudioInput extends ChangeNotifier { + AudioRecorder? _recorder; + RecordConfig recordConfig = RecordConfig( + encoder: AudioEncoder.pcm16bits, + sampleRate: 16000, + numChannels: 1, + echoCancel: true, + noiseSuppress: true, + androidConfig: AndroidRecordConfig( + audioSource: AndroidAudioSource.voiceCommunication, + ), + iosConfig: IosRecordConfig(categoryOptions: []), + ); + bool isRecording = false; + bool isPaused = false; + late Stream audioStream; + Stream? amplitudeStream; + StreamSubscription? _amplitudeSubscription; + StreamController? _amplitudeStreamController; + + Future init() async { + _recorder = AudioRecorder(); + await checkPermission(); + } + + @override + void dispose() { + _recorder?.dispose(); + super.dispose(); + } + + Future checkPermission() async { + final hasPermission = await _recorder!.hasPermission(); + if (!hasPermission) { + throw MicrophonePermissionDeniedException( + 'This app does not have microphone permissions. Please enable it.', + ); + } + } + + Future> startRecordingStream() async { + final devices = await _recorder!.listInputDevices(); + log(devices.toString()); + audioStream = (await _recorder!.startStream( + recordConfig, + )).asBroadcastStream(); + _amplitudeStreamController = StreamController.broadcast(); + _amplitudeSubscription = _recorder! + .onAmplitudeChanged(const Duration(milliseconds: 100)) + .listen((amp) { + _amplitudeStreamController?.add( + wf.Amplitude(current: amp.current, max: amp.max), + ); + }); + amplitudeStream = _amplitudeStreamController?.stream; + isRecording = true; + //log("${isRecording ? "Is" : "Not"} Recording"); + notifyListeners(); + return audioStream; + } + + Future stopRecording() async { + await _recorder!.stop(); + isRecording = false; + await _amplitudeSubscription?.cancel(); + await _amplitudeStreamController?.close(); + amplitudeStream = null; + _recorder?.dispose(); + _recorder = AudioRecorder(); + //log("${isRecording ? "Is" : "Not"} Recording"); + notifyListeners(); + } + + Future togglePauseRecording() async { + isPaused ? await _recorder!.resume() : await _recorder!.pause(); + isPaused = !isPaused; + notifyListeners(); + return; + } +} + +/// An exception thrown when microphone permission is denied or not granted. +class MicrophonePermissionDeniedException implements Exception { + /// The optional message associated with the permission denial. + final String? message; + + /// Creates a new [MicrophonePermissionDeniedException] with an optional [message]. + MicrophonePermissionDeniedException([this.message]); + + @override + String toString() { + if (message == null) { + return 'MicrophonePermissionDeniedException'; + } + return 'MicrophonePermissionDeniedException: $message'; + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_output.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_output.dart new file mode 100644 index 0000000..052304d --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_output.dart @@ -0,0 +1,98 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:flutter_soloud/flutter_soloud.dart'; + +class AudioOutput { + var initialized = false; + AudioSource? stream; + SoundHandle? handle; + final int sampleRate = 24000; + final Channels channels = Channels.mono; + final BufferType format = BufferType.s16le; // pcm16bits + + Future init() async { + if (initialized) { + return; + } + + /// Initialize the player (singleton). + await SoLoud.instance.init(sampleRate: sampleRate, channels: channels); + initialized = true; + } + + Future dispose() async { + if (initialized) { + SoLoud.instance.disposeAllSources(); + SoLoud.instance.deinit(); + initialized = false; + } + } + + SoLoud get instance => SoLoud.instance; + + AudioSource? setupNewStream() { + if (!SoLoud.instance.isInitialized) { + return null; + } + + stream = SoLoud.instance.setBufferStream( + bufferingType: BufferingType.released, + bufferingTimeNeeds: 0, + sampleRate: sampleRate, + channels: channels, + format: format, + onBuffering: (isBuffering, handle, time) { + log('Buffering: $isBuffering, Time: $time'); + }, + ); + log("New audio output stream buffer created."); + return stream; + } + + Future playStream() async { + var myStream = setupNewStream(); + if (!SoLoud.instance.isInitialized || myStream == null) { + return null; + } + // Play audio stream + handle = await SoLoud.instance.play(myStream); + stream = myStream; + return stream; + } + + void addDataToAudioStream(Uint8List audioChunk) { + var currentStream = stream; + if (currentStream != null) { + SoLoud.instance.addAudioDataStream(currentStream, audioChunk); + } + } + + Future stopStream() async { + var currentStream = stream; + var currentHandle = handle; + + // Stream doesn't exist or handle is not valid - so nothing to stop. + if (currentStream == null || + currentHandle == null || + !SoLoud.instance.getIsValidVoiceHandle(currentHandle)) { + return; + } + // End data to stream & stop currently playing sound from handle + SoLoud.instance.setDataIsEnded(currentStream); + await SoLoud.instance.stop(currentHandle); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/utilities.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/utilities.dart new file mode 100644 index 0000000..a19db32 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/utilities.dart @@ -0,0 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export './audio_input.dart'; +export './audio_output.dart'; +export './video_input.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/video_input.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/video_input.dart new file mode 100644 index 0000000..07891a6 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/video_input.dart @@ -0,0 +1,135 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'dart:async'; +import 'dart:typed_data'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +class VideoInput extends ChangeNotifier { + late List _cameras; + CameraController? _cameraController; + CameraDescription? _selectedCamera; + bool controllerInitialized = false; + Timer? _captureTimer; + StreamController _imageStreamController = StreamController(); + bool _isStreaming = false; + + List get cameras => _cameras; + CameraController? get cameraController => _cameraController; + + Future init() async { + try { + _cameras = await availableCameras(); + if (_cameras.isNotEmpty) { + _selectedCamera = _cameras[0]; + } + } catch (e) { + log('Error getting available cameras: $e'); + } + } + + @override + void dispose() { + super.dispose(); + stopStreamingImages(); + if (controllerInitialized && _cameraController != null) { + _cameraController!.dispose(); + } + } + + Future initializeCameraController() async { + var cameraController = _cameraController; + if (controllerInitialized && cameraController != null) { + await cameraController.dispose(); + controllerInitialized = false; + } + + if (_selectedCamera == null) { + log("No camera selected or available."); + return; + } + + _cameraController = CameraController( + _selectedCamera!, + ResolutionPreset.veryHigh, + enableAudio: false, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + try { + await _cameraController!.initialize(); + controllerInitialized = true; + notifyListeners(); + } catch (e) { + log('Error initializing camera: $e'); + } + } + + Stream startStreamingImages() { + if (_cameraController == null || !_cameraController!.value.isInitialized) { + throw ErrorSummary('Unable to start image stream'); + } + + _captureTimer = Timer.periodic( + const Duration(seconds: 1), // Capture images at 1 frame per second + (timer) async { + if (_cameraController == null || + !_cameraController!.value.isInitialized || + !_isStreaming) { + log("Stopping timer due to invalid state."); + stopStreamingImages(); + return; + } + + try { + // Prevent taking picture if already taking one + if (_cameraController!.value.isTakingPicture) { + return; + } + log("Taking picture..."); + final XFile imageFile = await _cameraController!.takePicture(); + Uint8List imageBytes = await imageFile.readAsBytes(); + _imageStreamController.add(imageBytes); + } catch (e) { + log('Error taking picture: $e'); + } + }, + ); + _isStreaming = true; + return _imageStreamController.stream; + } + + /// Stops the periodic image capture and closes the stream. + Future stopStreamingImages() async { + if (!_isStreaming) { + return; // Nothing to stop + } + _captureTimer?.cancel(); + await _imageStreamController.close(); + _imageStreamController = StreamController(); + _isStreaming = false; + } + + Future flipCamera() async { + if (_cameras.length > 1) { + final otherCamera = _cameras.firstWhere( + (camera) => camera.lensDirection != _selectedCamera?.lensDirection, + orElse: () => _cameras[0], + ); + _selectedCamera = otherCamera; + await initializeCameraController(); + } + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/firebaseai_multimodal_service.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/firebaseai_multimodal_service.dart new file mode 100644 index 0000000..40df704 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/firebaseai_multimodal_service.dart @@ -0,0 +1,53 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'models/attachment.dart'; + +/// A service that handles all communication with the Firebase AI Gemini API +/// for the Multimodal demo. +/// +/// This service demonstrates how to use the `generateContent()` method on a +/// `GenerativeModel` to provide multimodal input, combining text and file +/// data (like images or PDFs) in a single prompt. +/// +/// For more informations, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/generate-text?api=dev#base64 +class MultimodalService { + final _model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-3.5-flash', + ); + + /// Generates text content from a text prompt and a file attachment. + /// + /// Throws an exception if the API call fails. + Future generateContent(String prompt, Attachment attachment) async { + try { + final attachmentPart = InlineDataPart( + attachment.mimeType, + attachment.fileBytes, + ); + + final response = await _model.generateContent([ + Content.multi([TextPart(prompt), attachmentPart]), + ]); + + return response.text; + } catch (e) { + log('Error generating content: $e'); + rethrow; + } + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/models/attachment.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/models/attachment.dart new file mode 100644 index 0000000..f1c2fd3 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/models/attachment.dart @@ -0,0 +1,27 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; + +class Attachment { + String fileName; + String mimeType; + Uint8List fileBytes; + + Attachment({ + required this.fileName, + required this.mimeType, + required this.fileBytes, + }); +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/multimodal_demo.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/multimodal_demo.dart new file mode 100644 index 0000000..d1f0dfd --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/multimodal_demo.dart @@ -0,0 +1,124 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'dart:developer' as dev; +import '../../shared/ui/app_frame.dart'; +import '../../shared/ui/app_spacing.dart'; +import './models/attachment.dart'; +import './firebaseai_multimodal_service.dart'; +import './ui_components/ui_components.dart'; +import 'utilities/file_picker_utility.dart'; + +class MultimodalDemo extends StatefulWidget { + const MultimodalDemo({super.key}); + + @override + State createState() => _MultimodalDemoState(); +} + +class _MultimodalDemoState extends State { + // Service for interacting with the Gemini API. + final _multimodalService = MultimodalService(); + + // UI State + bool _loading = false; + TextEditingController promptController = TextEditingController( + text: 'Please analyze this file and explain it to me like I\'m 5.', + ); + Attachment? _attachment; + ExpansibleController promptTileController = ExpansibleController(); + String? outputText; + + void _pickFile() async { + final newAttachment = await FilePickerService().pickFile(context); + if (newAttachment != null) { + setState(() { + _attachment = newAttachment; + }); + } + } + + void askGemini() async { + setState(() { + _loading = true; + }); + + var attachment = _attachment; + var prompt = promptController.text.trim(); + + if (attachment == null || prompt.isEmpty) { + setState(() { + _loading = false; + }); + return; + } + + promptTileController.collapse(); + + try { + outputText = await _multimodalService.generateContent(prompt, attachment); + } catch (e) { + dev.log(e.toString()); + outputText = 'Oops, sorry there was an error processing that file.'; + promptTileController.expand(); + } finally { + setState(() { + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + ), + child: AppFrame( + child: Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.s8, + 0, + AppSpacing.s8, + AppSpacing.s8, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox.square(dimension: AppSpacing.s16), + FilePromptInput( + promptController: promptController, + tileController: promptTileController, + loading: _loading, + attachment: _attachment, + askGemini: askGemini, + onPickFilePressed: _pickFile, + onAttachmentChanged: (attachment) { + setState(() { + _attachment = attachment; + }); + }, + ), + OutputDisplay(loading: _loading, outputText: outputText), + ], + ), + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/attachment_view.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/attachment_view.dart new file mode 100644 index 0000000..b12e214 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/attachment_view.dart @@ -0,0 +1,218 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../models/attachment.dart'; + +class AttachmentView extends StatelessWidget { + final Attachment? attachment; + + const AttachmentView({super.key, this.attachment}); + + @override + Widget build(BuildContext context) { + return attachment == null + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + size: 81, + color: Theme.of(context).colorScheme.outline, + Icons.attach_file, + ), + const SizedBox.square(dimension: AppSpacing.s16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.s16), + child: Text( + 'Select a file', + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildPreview(context), + const SizedBox.square(dimension: AppSpacing.s16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.s16), + child: Text( + attachment!.fileName, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + + Widget _buildPreview(BuildContext context) { + final mimeType = attachment!.mimeType.toLowerCase(); + + if (mimeType.startsWith('image/')) { + return Container( + width: 160, + height: 160, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 1, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.memory( + attachment!.fileBytes, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => _buildIconFallback( + context, + Icons.broken_image, + Colors.red, + 'Image', + ), + ), + ), + ); + } + + if (mimeType.contains('pdf')) { + return _buildIconFallback( + context, + Icons.picture_as_pdf, + Colors.red.shade600, + 'PDF', + ); + } + + if (mimeType.startsWith('video/')) { + return _buildIconFallback( + context, + Icons.video_file, + Colors.indigo.shade600, + 'Video', + ); + } + + if (mimeType.startsWith('audio/')) { + return _buildIconFallback( + context, + Icons.audiotrack, + Colors.amber.shade700, + 'Audio', + ); + } + + if (mimeType.startsWith('text/') || + mimeType.contains('json') || + mimeType.contains('javascript') || + mimeType.contains('xml')) { + return _buildIconFallback( + context, + Icons.code, + Colors.teal.shade600, + 'Code', + ); + } + + // Default document fallback + return _buildIconFallback( + context, + Icons.insert_drive_file, + Colors.blueGrey.shade600, + 'File', + ); + } + + Widget _buildIconFallback( + BuildContext context, + IconData icon, + Color primaryColor, + String label, + ) { + return Container( + width: 160, + height: 160, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + primaryColor.withOpacity(0.15), + primaryColor.withOpacity(0.05), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: primaryColor.withOpacity(0.3), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.05), + blurRadius: 12, + spreadRadius: 2, + offset: const Offset(0, 4), + ), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + icon, + size: 64, + color: primaryColor, + ), + Positioned( + bottom: 12, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label.toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w800, + letterSpacing: 1.2, + ), + ), + ), + ), + ], + ), + ); + } +} + diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/dashed_border_painter.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/dashed_border_painter.dart new file mode 100644 index 0000000..73b7ce9 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/dashed_border_painter.dart @@ -0,0 +1,69 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; +import 'package:flutter/material.dart'; + +class DashedBorderPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double dashWidth; + final double dashSpace; + final Radius radius; + + DashedBorderPainter({ + required this.color, + this.strokeWidth = 1.0, + this.dashWidth = 8.0, + this.dashSpace = 8.0, + this.radius = const Radius.circular(0), + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; + + final path = Path() + ..addRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + radius, + ), + ); + + final dashPath = Path(); + double distance = 0.0; + + for (final pathMetric in path.computeMetrics()) { + while (distance < pathMetric.length) { + dashPath.addPath( + pathMetric.extractPath( + distance, + min(distance + dashWidth, pathMetric.length), + ), + Offset.zero, + ); + distance += dashWidth + dashSpace; + } + } + + canvas.drawPath(dashPath, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/file_prompt_input.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/file_prompt_input.dart new file mode 100644 index 0000000..0c9e99f --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/file_prompt_input.dart @@ -0,0 +1,178 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../models/attachment.dart'; +import './attachment_view.dart'; +import './dashed_border_painter.dart'; + +class FilePromptInput extends StatelessWidget { + final TextEditingController promptController; + final ExpansibleController tileController; + final bool loading; + final Attachment? attachment; + final void Function() askGemini; + final void Function(Attachment?) onAttachmentChanged; + final VoidCallback onPickFilePressed; + + const FilePromptInput({ + super.key, + required this.promptController, + required this.tileController, + required this.loading, + required this.attachment, + required this.askGemini, + required this.onAttachmentChanged, + required this.onPickFilePressed, + }); + + @override + Widget build(BuildContext context) { + return ExpansionTile( + controller: tileController, + collapsedShape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + title: Text( + style: Theme.of(context).textTheme.titleMedium, + 'File & Prompt', + ), + initiallyExpanded: true, + collapsedBackgroundColor: Theme.of(context).colorScheme.primaryContainer, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s16, + horizontal: AppSpacing.s24, + ), + child: Stack( + children: [ + Center( + child: GestureDetector( + onTap: onPickFilePressed, + child: CustomPaint( + painter: DashedBorderPainter( + color: Theme.of(context).colorScheme.outline, + strokeWidth: attachment == null ? 4 : 0, + radius: Radius.circular(AppSpacing.s16), + ), + child: Container( + width: 240, + height: 240, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + child: AttachmentView(attachment: attachment), + ), + ), + ), + ), + if (attachment != null) + Column( + children: [ + Center( + child: SizedBox( + width: 240, + height: 240, + child: Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: IconButton( + onPressed: () => onAttachmentChanged(null), + icon: Icon( + size: 32, + color: Theme.of(context).colorScheme.error, + Icons.close, + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox.square(dimension: AppSpacing.s24), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: TextField( + decoration: InputDecoration( + label: const Text('Prompt'), + fillColor: Theme.of(context).colorScheme.onSecondaryFixed, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + maxLines: 4, + controller: promptController, + enabled: !loading, + onTap: () { + promptController.selection = TextSelection( + baseOffset: 0, + extentOffset: promptController.text.length, + ); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: ElevatedButton( + style: ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + backgroundColor: WidgetStatePropertyAll( + Theme.of(context).colorScheme.primaryContainer, + ), + ), + onPressed: loading ? null : askGemini, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s24, + horizontal: 0, + ), + child: Column( + children: [ + SizedBox( + width: 42, + height: 42, + child: Image.asset('assets/gemini-logo.png'), + ), + const SizedBox.square(dimension: AppSpacing.s4), + const Text(textAlign: TextAlign.center, 'Ask\nGemini'), + ], + ), + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/output_display.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/output_display.dart new file mode 100644 index 0000000..0794462 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/output_display.dart @@ -0,0 +1,44 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class OutputDisplay extends StatelessWidget { + final bool loading; + final String? outputText; + + const OutputDisplay({super.key, required this.loading, this.outputText}); + + @override + Widget build(BuildContext context) { + return loading + ? Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: const LinearProgressIndicator(), + ) + : outputText != null + ? Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.s24, + AppSpacing.s24, + AppSpacing.s24, + AppSpacing.s48, + ), + child: MarkdownBody(data: outputText!), + ) + : Container(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/ui_components.dart new file mode 100644 index 0000000..0f14a00 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/ui_components.dart @@ -0,0 +1,18 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'attachment_view.dart'; +export 'dashed_border_painter.dart'; +export 'file_prompt_input.dart'; +export 'output_display.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/utilities/file_picker_utility.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/utilities/file_picker_utility.dart new file mode 100644 index 0000000..2395c8b --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/utilities/file_picker_utility.dart @@ -0,0 +1,158 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:io'; +import 'dart:developer'; +import 'package:cross_file/cross_file.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:mime/mime.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../models/attachment.dart'; + +const List imageFileTypes = ['png', 'jpeg', 'webp', 'jpg']; +const List videoFileTypes = [ + 'flv', + 'mov', + 'mpeg', + 'mpegps', + 'mpg', + 'mp4', + 'webm', + 'wmv', + '3gpp', +]; +const List audioFileTypes = [ + 'aac', + 'flac', + 'mp3', + 'm4a', + 'mpeg', + 'mpga', + 'mp4', + 'opus', + 'pcm', + 'wav', + 'webm', +]; +const List textFileTypes = ['pdf', 'txt']; + +class FilePickerService { + Future pickFile(BuildContext context) async { + final String? source = await _showFileSourcePicker(context); + if (source == null) return null; + + final FilePickerResult? result = await _pickFileFromSource(source); + if (result == null) return null; + + return _processFilePickerResult(result); + } + + Future _showFileSourcePicker(BuildContext context) async { + return await showModalBottomSheet( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + context: context, + builder: (context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: SizedBox( + height: 240, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.s16, + vertical: AppSpacing.s8, + ), + child: Text( + style: Theme.of(context).textTheme.titleLarge, + 'File selection', + ), + ), + const Divider(), + if (!kIsWeb && Platform.isIOS) + ListTile( + leading: const Icon(size: 24, Icons.photo_library_rounded), + onTap: () { + Navigator.pop(context, 'Library'); + }, + title: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: Text( + style: Theme.of(context).textTheme.titleMedium, + 'Photos and videos', + ), + ), + ), + ListTile( + leading: const Icon(size: 24, Icons.folder), + onTap: () { + Navigator.pop(context, 'Files'); + }, + title: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: Text( + style: Theme.of(context).textTheme.titleMedium, + 'Browse files', + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Future _pickFileFromSource(String source) async { + if (source == 'Files') { + return await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: [ + ...audioFileTypes, + ...imageFileTypes, + ...videoFileTypes, + ...textFileTypes, + ], + ); + } else { + return await FilePicker.platform.pickFiles(type: FileType.media); + } + } + + Future _processFilePickerResult(FilePickerResult result) async { + var file = XFile(result.files.single.path!); + String fileName = result.files.single.name; + Uint8List fileBytes = await file.readAsBytes(); + String? mimeType = lookupMimeType( + fileName, + headerBytes: fileBytes.sublist(0, 10), + ); + + if (mimeType != null) { + return Attachment( + fileName: fileName, + mimeType: mimeType, + fileBytes: fileBytes, + ); + } else { + // Could not determine the file type. + log('Could not determine MIME type for ${file.path}'); + } + return null; + } +} diff --git a/firebase_ai_logic_showcase/lib/firebase_options.dart b/firebase_ai_logic_showcase/lib/firebase_options.dart new file mode 100644 index 0000000..a2ed568 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/firebase_options.dart @@ -0,0 +1,75 @@ +import 'dart:js_interop'; + +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show TargetPlatform, defaultTargetPlatform, kIsWeb; + +extension type BootstrapFirebaseOptions._(JSObject _) implements JSObject { + external String get apiKey; + external String get authDomain; + external String get databaseURL; + external String get projectId; + external String get storageBucket; + external String get messagingSenderId; + external String get appId; + external String get measurementId; +} + +extension type BootstrapOptions._(JSObject _) implements JSObject { + external String get geminiApiKey; + external BootstrapFirebaseOptions get firebase; +} + +@JS() +// ignore: non_constant_identifier_names +external BootstrapOptions get APP_TEMPLATE_BOOTSTRAP; + +class DefaultFirebaseOptions { + + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for android - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.iOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for ios - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + // ignore: no_default_cases + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static FirebaseOptions web = FirebaseOptions( + apiKey: APP_TEMPLATE_BOOTSTRAP.firebase.apiKey, + appId: APP_TEMPLATE_BOOTSTRAP.firebase.appId, + messagingSenderId: APP_TEMPLATE_BOOTSTRAP.firebase.messagingSenderId, + projectId: APP_TEMPLATE_BOOTSTRAP.firebase.projectId, + authDomain: APP_TEMPLATE_BOOTSTRAP.firebase.authDomain, + storageBucket: APP_TEMPLATE_BOOTSTRAP.firebase.storageBucket, + ); +} diff --git a/firebase_ai_logic_showcase/lib/flutter_firebase_ai_demo.dart b/firebase_ai_logic_showcase/lib/flutter_firebase_ai_demo.dart new file mode 100644 index 0000000..a500f9e --- /dev/null +++ b/firebase_ai_logic_showcase/lib/flutter_firebase_ai_demo.dart @@ -0,0 +1,129 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'demos/chat/chat_demo.dart'; +import 'demos/chat_nano/chat_nano_demo.dart'; +import 'demos/multimodal/multimodal_demo.dart'; +import 'demos/live_api/live_api_demo.dart'; + +class DemoHomeScreen extends StatefulWidget { + const DemoHomeScreen({super.key}); + + @override + State createState() => _DemoHomeScreenState(); +} + +class _DemoHomeScreenState extends State { + int _selectedIndex = 0; + final List<({Widget icon, String label, Widget? selectedIcon})> + destinations = [ + (icon: const Icon(Icons.chat), label: 'Chat', selectedIcon: null), + /* TODO: This demo should remain inactive until Appcheck is enforced by default @cynthiajoan + ( + icon: const Icon(Icons.video_chat), + label: 'Live API', + selectedIcon: null, + ),*/ + ( + icon: const Icon(Icons.photo_library), + label: 'Multimodal', + selectedIcon: null, + ), + ( + icon: RichText( + text: const TextSpan(style: TextStyle(fontSize: 24.0), text: '🍌'), + ), + label: 'Nano Banana', + selectedIcon: null, + ), + ]; + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return Scaffold( + body: Row( + children: [ + if (constraints.maxWidth > + 600) // Show NavigationRail on Medium or larger screens + ...[ + NavigationRail( + leading: Padding( + padding: EdgeInsets.all(16), + child: Image.asset( + 'assets/firebase-ai-logic.png', + width: 45, + height: 45, + ), + ), + selectedIndex: _selectedIndex, + onDestinationSelected: _onItemTapped, + labelType: NavigationRailLabelType.all, + destinations: destinations + .map( + (e) => NavigationRailDestination( + padding: const EdgeInsets.symmetric(vertical: 8.0), + icon: e.icon, + label: Text( + e.label.replaceAll(' ', '\n'), + textAlign: TextAlign.center, + ), + selectedIcon: e.selectedIcon, + ), + ) + .toList(), + ), + VerticalDivider(thickness: 1, width: 1), + ], + Expanded(child: demoPages[_selectedIndex]), + ], + ), + bottomNavigationBar: + (constraints.maxWidth < + 600) // Show navigation bar on smaller screens + ? BottomNavigationBar( + type: BottomNavigationBarType.fixed, + items: destinations + .map( + (e) => BottomNavigationBarItem( + icon: e.icon, + label: e.label, + activeIcon: e.selectedIcon, + ), + ) + .toList(), + currentIndex: _selectedIndex, + onTap: _onItemTapped, + ) + : null, + ); + }, + ); + } +} + +final List demoPages = [ + const ChatDemo(), + // TODO: This demo should remain inactive until Appcheck is enforced by default @cynthiajoan LiveAPIDemo(), + const MultimodalDemo(), + ChatDemoNano(), +]; diff --git a/firebase_ai_logic_showcase/lib/main.dart b/firebase_ai_logic_showcase/lib/main.dart new file mode 100644 index 0000000..cb2a0d0 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/main.dart @@ -0,0 +1,53 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'flutter_firebase_ai_demo.dart'; +import './firebase_options.dart'; +import 'shared/app_state.dart'; + +void main() async { + FirebaseOptions options = DefaultFirebaseOptions.currentPlatform; + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: options); + runApp(const ProviderScope(child: MyApp())); +} + +class MyApp extends ConsumerWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appColor = ref.watch(appStateProvider).appColor; + return MaterialApp( + title: 'Flutter AI Playground', + home: DemoHomeScreen(), + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: appColor, + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.tonalSpot, + ).copyWith(surface: appColor), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: Colors.grey.shade900, + selectedItemColor: Colors.white, + unselectedItemColor: Colors.grey.shade400, + ), + ), + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/app_state.dart b/firebase_ai_logic_showcase/lib/shared/app_state.dart new file mode 100644 index 0000000..a049d3a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/app_state.dart @@ -0,0 +1,30 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AppState extends ChangeNotifier { + Color appColor = Color.fromARGB(255, 0, 32, 46); + + String setAppColor(Color color) { + appColor = color; + notifyListeners(); + return 'Color successfully changed to ${appColor.toString()}!'; + } +} + +final appStateProvider = ChangeNotifierProvider((ref) { + return AppState(); +}); diff --git a/firebase_ai_logic_showcase/lib/shared/chat_service.dart b/firebase_ai_logic_showcase/lib/shared/chat_service.dart new file mode 100644 index 0000000..eaf5ba1 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/chat_service.dart @@ -0,0 +1,120 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../shared/app_state.dart'; +import '../shared/firebaseai_imagen_service.dart'; +import './models/models.dart'; + +/// A service that handles all communication with the Firebase AI Gemini API +/// for the Chat Demo. +/// +/// This service demonstrates how to use the `startChat()` method on a +/// `GenerativeModel` to create a persistent conversation. The `ChatSession` +/// object automatically handles the conversation history, making it easy to +/// build multi-turn chat experiences. +/// +/// For more information, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/chat?api=dev +class ChatService { + final WidgetRef _ref; + GeminiModel? _gemini; + + ChatService(this._ref, [this._gemini]); + + late ChatSession _chat; + + void init() { + var gemini = _gemini ?? geminiModels.selectedModel; + _chat = gemini.model.startChat(); + } + + void changeModel(String modelName) { + _gemini = geminiModels.selectModel(modelName); + init(); + } + + Future sendMessage(Content message) async { + try { + var response = await _chat.sendMessage(message); + + if (response.functionCalls.isNotEmpty) { + return _handleFunctionCall(response.functionCalls); + } else { + if (response.inlineDataParts.isNotEmpty) { + final imageBytes = response.inlineDataParts.first.bytes; + var image = Image.memory(imageBytes); + return ChatResponse(text: response.text, image: image); + } + + return ChatResponse(text: response.text); + } + } catch (e) { + log('Error sending message: $e'); + rethrow; + } + } + + Future _handleFunctionCall( + Iterable functionCalls, + ) async { + var functionCall = functionCalls.first; + log("Gemini made a function call: ${functionCall.name}"); + + switch (functionCall.name) { + case 'SetAppColor': + final response = await _handleSetAppColor(functionCall); + return ChatResponse(text: response.text); + case 'GenerateImage': + return await _handleGenerateImage(functionCall); + default: + final response = await _chat.sendMessage( + Content.text( + 'Function Call name was not found! Please try another function call.', + ), + ); + return ChatResponse(text: response.text); + } + } + + Future _handleSetAppColor( + FunctionCall functionCall, + ) async { + log('Set app color!'); + int red = functionCall.args['red']! as int; + int green = functionCall.args['green']! as int; + int blue = functionCall.args['blue']! as int; + var newSeedColor = Color.fromRGBO(red, green, blue, 1); + var executedFunctionCall = _ref + .read(appStateProvider) + .setAppColor(newSeedColor); + return await _chat.sendMessage(Content.text(executedFunctionCall)); + } + + Future _handleGenerateImage(FunctionCall functionCall) async { + log('Generate image!'); + String description = functionCall.args['description']! as String; + var imageBytes = await ImageGenerationService().generateImage(description); + var response = await _chat.sendMessage( + Content.text( + 'Successfully generated an image of $description! Please send back a message to include with the image.', + ), + ); + var responseImage = Image.memory(imageBytes); + return ChatResponse(text: response.text, image: responseImage); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/firebaseai_imagen_service.dart b/firebase_ai_logic_showcase/lib/shared/firebaseai_imagen_service.dart new file mode 100644 index 0000000..c251284 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/firebaseai_imagen_service.dart @@ -0,0 +1,48 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:firebase_ai/firebase_ai.dart'; + +/// This service demonstrates how to use a Gemini Image model to generate images +/// from a text prompt. It showcases the text-to-image generation feature. +/// +/// For more information, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/generate-images-imagen?api=dev +/// +/// This is a shared service, located in the /shared directory, because it is +/// used by multiple demos (Chat, Live API, and Imagen) to provide image +/// generation capabilities. +class ImageGenerationService { + final _model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-3.1-flash-image-preview', + generationConfig: GenerationConfig( + responseModalities: [ResponseModalities.image], + ), + ); + + /// Generates a single image from a text prompt. + /// + /// Throws an exception if the API call fails, allowing the UI to handle it. + Future generateImage(String prompt) async { + try { + final res = await _model.generateContent([Content.text(prompt)]); + return res.inlineDataParts.first.bytes; + } catch (e) { + log('Error generating image: $e'); + rethrow; + } + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/function_calling/tools.dart b/firebase_ai_logic_showcase/lib/shared/function_calling/tools.dart new file mode 100644 index 0000000..86f15ac --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/function_calling/tools.dart @@ -0,0 +1,47 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:firebase_ai/firebase_ai.dart'; + +final generateImageTool = FunctionDeclaration( + 'GenerateImage', + 'Generate an image by describing it.', + parameters: { + 'description': Schema.string( + description: + 'A description of the image that you want to generate. Be as specific as possible. More specific is better. Describe the contents of the image, the style, and any other specifics about the image that is to be generated.', + ), + }, +); + +final setAppColorTool = FunctionDeclaration( + 'SetAppColor', + 'Set the app color. You must pick a color that matches the hue that the user requests. When talking with the user, use a human-friendly description of the color instead of RGB values.', + parameters: { + 'red': Schema.integer( + description: + "The desired app color's RGB RED channel value that can range from 0 to 46", + ), + 'green': Schema.integer( + description: + "The desired app color's RGB GREEN channel value that can range from 0 to 46", + ), + 'blue': Schema.integer( + description: + "The desired app color's RGB BLUE channel that can range from 0 to 46", + ), + }, +); + +// See "live_api" and "chat" demos for full implementation of function calling. diff --git a/firebase_ai_logic_showcase/lib/shared/models/chat_response.dart b/firebase_ai_logic_showcase/lib/shared/models/chat_response.dart new file mode 100644 index 0000000..e46bd91 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/models/chat_response.dart @@ -0,0 +1,23 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +/// A simple container for the response from the ChatService. +class ChatResponse { + final String? text; + final Image? image; + + ChatResponse({this.text, this.image}); +} diff --git a/firebase_ai_logic_showcase/lib/shared/models/gemini_model.dart b/firebase_ai_logic_showcase/lib/shared/models/gemini_model.dart new file mode 100644 index 0000000..627e588 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/models/gemini_model.dart @@ -0,0 +1,105 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:firebase_ai/firebase_ai.dart'; +import '../../../shared/function_calling/tools.dart'; + +var geminiModels = GeminiModels(); + +class GeminiModel { + final String name; + final String description; + final GenerativeModel model; + final String defaultPrompt; + + GeminiModel({ + required this.name, + required this.description, + required this.model, + required this.defaultPrompt, + }); +} + +class GeminiModels { + String selectedModelName = 'gemini-3.1-flash-image-preview'; + GeminiModel get selectedModel => models[selectedModelName]!; + + /// A map of Gemini models that can be used in the Chat Demo. + Map models = { + 'gemini-3.5-flash': GeminiModel( + name: 'gemini-3.5-flash', + description: + 'Our thinking model that offers great, well-rounded capabilities. It\'s designed to offer a balance between price and performance.', + model: FirebaseAI.googleAI().generativeModel( + model: 'gemini-3.5-flash', + tools: [ + Tool.functionDeclarations([setAppColorTool, generateImageTool]), + ], + generationConfig: GenerationConfig( + responseModalities: [ResponseModalities.text], + ), + ), + defaultPrompt: 'Hey Gemini! Can you set the app color to purple?', + ), + 'gemini-3.1-flash-image-preview': GeminiModel( + name: 'gemini-3.1-flash-image-preview', + description: + 'Our standard Flash model upgraded for rapid creative workflows with image generation and conversational, multi-turn editing capabilities.', + model: FirebaseAI.googleAI().generativeModel( + model: 'gemini-3.1-flash-image-preview', + generationConfig: GenerationConfig( + responseModalities: [ + ResponseModalities.text, + ResponseModalities.image, + ], + ), + ), + defaultPrompt: + 'Hot air balloons rising over the San Francisco Bay at golden hour ' + 'with a view of the Golden Gate Bridge. Make it anime style.', + ), + 'gemini-3-pro-image-preview': GeminiModel( + name: 'gemini-3-pro-image-preview', + description: + 'Gemini 3 Pro Image (aka nano banana pro). Designed for professional ' + 'asset production and complex instructions. It features real-world ' + 'grounding using Google Search, a default "Thinking" process that ' + 'refines composition prior to generation, and can generate images of up to 4K resolution.', + model: FirebaseAI.googleAI().generativeModel( + model: 'gemini-3-pro-image-preview', + generationConfig: GenerationConfig( + responseModalities: [ + ResponseModalities.text, + ResponseModalities.image, + ], + ), + ), + defaultPrompt: + 'Hot air balloons rising over the San Francisco Bay at golden hour ' + 'with a view of the Golden Gate Bridge. Make it anime style.', + ), + }; + + GeminiModel selectModel(String modelName) { + if (models.containsKey(modelName)) { + selectedModelName = modelName; + } else { + throw Exception('Model $modelName not found'); + } + return selectedModel; + } + + List get modelNames => models.keys.toList(); + GeminiModel operator [](String name) => models[name]!; +} diff --git a/firebase_ai_logic_showcase/lib/shared/models/models.dart b/firebase_ai_logic_showcase/lib/shared/models/models.dart new file mode 100644 index 0000000..4bc9e2a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/models/models.dart @@ -0,0 +1,16 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export './chat_response.dart'; +export './gemini_model.dart'; diff --git a/firebase_ai_logic_showcase/lib/shared/ui/app_frame.dart b/firebase_ai_logic_showcase/lib/shared/ui/app_frame.dart new file mode 100644 index 0000000..25772fe --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/app_frame.dart @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +class AppFrame extends StatelessWidget { + const AppFrame({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints.loose(Size(900, double.infinity)), + child: child, + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/app_spacing.dart b/firebase_ai_logic_showcase/lib/shared/ui/app_spacing.dart new file mode 100644 index 0000000..8aeff26 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/app_spacing.dart @@ -0,0 +1,35 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// A class to hold consistent spacing values for the application, +/// following a 4dp grid. +class AppSpacing { + /// 4.0 + static const double s4 = 4.0; + + /// 8.0 + static const double s8 = 8.0; + + /// 16.0 + static const double s16 = 16.0; + + /// 24.0 + static const double s24 = 24.0; + + /// 48.0 + static const double s48 = 48.0; + + /// 60.0 + static const double s60 = 60.0; +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/blaze_warning.dart b/firebase_ai_logic_showcase/lib/shared/ui/blaze_warning.dart new file mode 100644 index 0000000..cf9f875 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/blaze_warning.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/link.dart'; +import '../../firebase_options.dart'; + +class BlazeWarning extends StatelessWidget { + const BlazeWarning({super.key}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium, + text: + 'This demo includes some features that require an upgrade to the pay-as-you-go Blaze pricing plan, which you can ', + children: [ + WidgetSpan( + baseline: TextBaseline.ideographic, + alignment: PlaceholderAlignment.top, + child: Link( + uri: Uri.parse( + 'https://console.firebase.google.com/project/${DefaultFirebaseOptions.currentPlatform.projectId}/overview?purchaseBillingPlan=metered', + ), + target: LinkTarget.blank, + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + height: 1.15, + decoration: TextDecoration.underline, + color: Theme.of(context).colorScheme.primary, + ), + 'set up in the Firebase console', + ), + ), + ), + ), + TextSpan(text: '.'), + TextSpan(text: '\n\n'), + TextSpan(text: 'Eligible developers can claim '), + WidgetSpan( + baseline: TextBaseline.ideographic, + alignment: PlaceholderAlignment.top, + child: Link( + uri: Uri.parse( + 'https://firebase.blog/posts/2024/11/claim-300-to-get-started', + ), + target: LinkTarget.blank, + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + height: 1.15, + decoration: TextDecoration.underline, + color: Theme.of(context).colorScheme.primary, + ), + '\$300 of credits', + ), + ), + ), + ), + TextSpan(text: ' to get started.'), + ], + ), + ), + ), + ); + } +} + +class BlazeFooter extends StatelessWidget { + const BlazeFooter({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8.0), + child: Text.rich( + textAlign: TextAlign.center, + TextSpan( + style: Theme.of(context).textTheme.bodyMedium, + children: [ + TextSpan( + text: + '* This demo includes some features that require an upgrade to the ', + ), + WidgetSpan( + baseline: TextBaseline.ideographic, + alignment: PlaceholderAlignment.top, + child: Link( + uri: Uri.parse('https://firebase.google.com/pricing'), + target: LinkTarget.blank, + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + height: 1.15, + decoration: TextDecoration.underline, + color: Theme.of(context).colorScheme.primary, + ), + 'pay-as-you-go Blaze pricing plan', + ), + ), + ), + ), + TextSpan(text: ', which you can '), + WidgetSpan( + baseline: TextBaseline.ideographic, + alignment: PlaceholderAlignment.top, + child: Link( + uri: Uri.parse( + 'https://console.firebase.google.com/project/${DefaultFirebaseOptions.currentPlatform.projectId}/overview?purchaseBillingPlan=metered', + ), + target: LinkTarget.blank, + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + height: 1.15, + decoration: TextDecoration.underline, + color: Theme.of(context).colorScheme.primary, + ), + 'set up in the Firebase console', + ), + ), + ), + ), + TextSpan(text: '.'), + ], + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/chat_components/attachment_preview.dart b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/attachment_preview.dart new file mode 100644 index 0000000..18fed23 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/attachment_preview.dart @@ -0,0 +1,47 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class AttachmentPreview extends StatelessWidget { + final Uint8List? attachment; + + const AttachmentPreview({super.key, this.attachment}); + + @override + Widget build(BuildContext context) { + return attachment != null + ? Row( + children: [ + Padding( + padding: const EdgeInsets.only(top: AppSpacing.s16), + child: Container( + height: 95, + width: 95, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppSpacing.s8), + image: DecorationImage( + fit: BoxFit.cover, + image: MemoryImage(attachment!), + ), + ), + ), + ), + ], + ) + : Container(); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_bubble.dart b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_bubble.dart new file mode 100644 index 0000000..def2442 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_bubble.dart @@ -0,0 +1,70 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import './message_widget.dart'; + +class MessageBubble extends StatelessWidget { + final MessageData message; + + const MessageBubble({super.key, required this.message}); + + @override + Widget build(BuildContext context) { + final isFromUser = message.fromUser ?? false; + return ListTile( + minVerticalPadding: 4, + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadiusGeometry.circular(16), + ), + contentPadding: isFromUser + ? EdgeInsets.only(left: 16, top: 8, right: 8, bottom: 8) + : EdgeInsets.only(left: 8, top: 8, right: 16, bottom: 8), + leading: (!isFromUser) + ? CircleAvatar( + backgroundColor: Colors.transparent, + child: Image.asset('assets/gemini-logo.png'), + ) + : null, + title: Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: isFromUser + ? Theme.of(context).colorScheme.surfaceBright + : Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + if (message.text != null) + Align( + alignment: Alignment.centerLeft, + child: MarkdownBody(data: message.text!), + ), + if (message.image != null) + Padding( + padding: EdgeInsets.only(top: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadius.circular(16), + child: message.image!, + ), + ), + ], + ), + ), + ).animate().fadeIn().slideY().scaleXY(); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_input_bar.dart b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_input_bar.dart new file mode 100644 index 0000000..1ecd2d6 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_input_bar.dart @@ -0,0 +1,93 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class MessageInputBar extends StatelessWidget { + final TextEditingController textController; + final bool loading; + final void Function(String) sendMessage; + final VoidCallback onPickImagePressed; + + const MessageInputBar({ + super.key, + required this.textController, + required this.loading, + required this.sendMessage, + required this.onPickImagePressed, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(AppSpacing.s16), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1, + color: Theme.of(context).colorScheme.outline.withAlpha(125), + ), + ), + ), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.maybeViewInsetsOf(context)?.bottom ?? 0, + ), + child: SafeArea( + child: Row( + children: [ + IconButton( + onPressed: onPickImagePressed, + icon: const Icon(Icons.image), + ), + const SizedBox.square(dimension: AppSpacing.s8), + Expanded( + child: TextField( + onTapOutside: (PointerDownEvent event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + controller: textController, + minLines: 2, + maxLines: 2, + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: const BorderSide(width: 0), + borderRadius: BorderRadius.all( + Radius.circular(AppSpacing.s8), + ), + ), + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + ), + ), + ), + const SizedBox.square(dimension: AppSpacing.s16), + IconButton.filled( + onPressed: !loading + ? () => sendMessage(textController.text) + : null, + icon: const Icon(Icons.arrow_upward_rounded), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_list_view.dart b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_list_view.dart new file mode 100644 index 0000000..b78d8a0 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_list_view.dart @@ -0,0 +1,40 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import './message_bubble.dart'; +import './message_widget.dart'; + +class MessageListView extends StatelessWidget { + final List messages; + final ScrollController scrollController; + + const MessageListView({ + super.key, + required this.messages, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + controller: scrollController, + itemCount: messages.length, + itemBuilder: (context, idx) { + final message = messages[idx]; + return MessageBubble(message: message); + }, + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_widget.dart b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_widget.dart new file mode 100644 index 0000000..92fdf86 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/message_widget.dart @@ -0,0 +1,78 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class FunctionCallResponse { + final GenerateContentResponse? response; + final Image? image; + + FunctionCallResponse(this.response, this.image); +} + +class MessageData { + MessageData({this.image, this.text, this.fromUser}); + final Image? image; + final String? text; + final bool? fromUser; +} + +class MessageWidget extends StatelessWidget { + final Image? image; + final String? text; + final bool isFromUser; + + const MessageWidget({ + super.key, + this.image, + this.text, + required this.isFromUser, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: isFromUser + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + Flexible( + child: Container( + constraints: const BoxConstraints(maxWidth: 600), + decoration: BoxDecoration( + color: isFromUser + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s16, + horizontal: AppSpacing.s24, + ), + margin: const EdgeInsets.only(bottom: AppSpacing.s8), + child: Column( + children: [ + if (text case final text?) MarkdownBody(data: text), + if (image case final image?) image, + ], + ), + ), + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/chat_components/model_picker.dart b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/model_picker.dart new file mode 100644 index 0000000..1157537 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/model_picker.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../../../shared/ui/blaze_warning.dart'; +import '../../../shared/models/gemini_model.dart'; + +class ModelPicker extends StatefulWidget { + const ModelPicker({ + required this.selectedModel, + required this.onSelected, + super.key, + }); + + final GeminiModel selectedModel; + final Function(String value) onSelected; + + @override + State createState() => _ModelPickerState(); +} + +class _ModelPickerState extends State { + late String _selectedModelName; + late String _selectedModelDescription; + + @override + void initState() { + super.initState(); + _selectedModelName = widget.selectedModel.name; + _selectedModelDescription = widget.selectedModel.description; + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Theme.of(context).colorScheme.surface, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 600), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DropdownMenu( + label: const Text('Select a Gemini Model'), + initialSelection: _selectedModelName, + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + ), + fillColor: Theme.of(context).colorScheme.primaryContainer, + ), + dropdownMenuEntries: geminiModels.models.entries + .map( + (entry) => + DropdownMenuEntry(value: entry.key, label: entry.key), + ) + .toList(), + onSelected: (value) { + if (value != null) { + setState(() { + _selectedModelName = value; + _selectedModelDescription = + geminiModels[_selectedModelName].description; + }); + widget.onSelected(value); + } + }, + ), + const SizedBox.square(dimension: AppSpacing.s8), + Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: Text( + textAlign: TextAlign.center, + _selectedModelDescription, + ), + ), + if (_selectedModelName.contains('preview')) BlazeWarning(), + ], + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/chat_components/ui_components.dart b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/ui_components.dart new file mode 100644 index 0000000..8572431 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/chat_components/ui_components.dart @@ -0,0 +1,18 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'attachment_preview.dart'; +export 'message_input_bar.dart'; +export 'message_list_view.dart'; +export 'message_widget.dart'; diff --git a/firebase_ai_logic_showcase/pubspec.yaml b/firebase_ai_logic_showcase/pubspec.yaml new file mode 100644 index 0000000..85bf715 --- /dev/null +++ b/firebase_ai_logic_showcase/pubspec.yaml @@ -0,0 +1,42 @@ +name: flutter_firebase_ai_sample +description: "Flutter application demonstrates Firebase AI Logic capabilities through a series of interactive demos." +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.8.1 + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.8 + record: ^6.1.1 + flutter_riverpod: ^2.6.1 + flutter_soloud: ^3.1.6 + firebase_core: ^4.0.0 + camera: ^0.11.1 + flutter_image_compress: ^2.4.0 + waveform_flutter: ^1.2.0 + flutter_animate: ^4.5.2 + firebase_ai: ^3.3.0 + image_picker: ^1.1.2 + permission_handler: ^12.0.1 + flutter_markdown: ^0.7.7+1 + file_picker: ^10.2.1 + cross_file: ^0.3.4+2 + mime: ^2.0.0 + url_launcher: ^6.3.2 + + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true + assets: + - assets/gemini-logo.png + - assets/firebase-ai-logic.png + + diff --git a/firebase_ai_logic_showcase/run.sh b/firebase_ai_logic_showcase/run.sh new file mode 100755 index 0000000..e59dfdd --- /dev/null +++ b/firebase_ai_logic_showcase/run.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Set the predefined relative application path +APP_PATH="." +echo "APP_PATH is set to: ${APP_PATH}" + +# Define the bootstrap.js path +BOOTSTRAP_JS_PATH="${APP_PATH}/web/bootstrap.js" +echo "BOOTSTRAP_JS_PATH is set to: ${BOOTSTRAP_JS_PATH}" + +# Check if bootstrap.js exists +if [ ! -f "${BOOTSTRAP_JS_PATH}" ]; then + echo "Error: ${BOOTSTRAP_JS_PATH} not found. Please run configure.sh first." + exit 1 +fi + +echo "${BOOTSTRAP_JS_PATH} found." + +PID=$(sudo netstat -ltnp | grep :8080 | awk '{print $7}' | cut -d'/' -f1) + if [ -n "$PID" ]; then + echo "Killing process $PID on port 8080" + sudo kill -9 $PID +fi + +# Build the Flutter application +echo "Building the application..." +(cd "${APP_PATH}" && flutter build web --release) + +# Run Python web server +echo "Serving web application..." +(cd "${APP_PATH}/build/web" && python3 -m http.server 8080) diff --git a/firebase_ai_logic_showcase/web/404.html b/firebase_ai_logic_showcase/web/404.html new file mode 100644 index 0000000..829eda8 --- /dev/null +++ b/firebase_ai_logic_showcase/web/404.html @@ -0,0 +1,33 @@ + + + + + + Page Not Found + + + + +
+

404

+

Page Not Found

+

The specified file was not found on this website. Please check the URL for mistakes and try again.

+

Why am I seeing this?

+

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

+
+ + diff --git a/firebase_ai_logic_showcase/web/favicon.png b/firebase_ai_logic_showcase/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/firebase_ai_logic_showcase/web/favicon.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-192.png b/firebase_ai_logic_showcase/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-192.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-512.png b/firebase_ai_logic_showcase/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-512.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-maskable-192.png b/firebase_ai_logic_showcase/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-maskable-192.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-maskable-512.png b/firebase_ai_logic_showcase/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-maskable-512.png differ diff --git a/firebase_ai_logic_showcase/web/index.html b/firebase_ai_logic_showcase/web/index.html new file mode 100644 index 0000000..a7eed38 --- /dev/null +++ b/firebase_ai_logic_showcase/web/index.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + flutter_firebase_ai_sample + + + + + + + + + + + + + \ No newline at end of file diff --git a/firebase_ai_logic_showcase/web/manifest.json b/firebase_ai_logic_showcase/web/manifest.json new file mode 100644 index 0000000..1edb532 --- /dev/null +++ b/firebase_ai_logic_showcase/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "flutter_firebase_ai_sample", + "short_name": "flutter_firebase_ai_sample", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} \ No newline at end of file diff --git a/flutter-frontend-for-adk/README.md b/flutter-frontend-for-adk/README.md new file mode 100644 index 0000000..41dd54e --- /dev/null +++ b/flutter-frontend-for-adk/README.md @@ -0,0 +1,84 @@ +# Flutter Frontend for ADK Agents (Custom Skill) + +This repository contains a skill intended for use with [Antigravity](https://antigravity.google). It will guide the agent through the discovery, architecture, design, and implementation of a Flutter-based frontend for an Agent Development Kit (ADK) agent written in Python. + +> [!NOTE] +> This repository is a learning resource and an open-source sample. Use it with an experimental project as a way of learning how to use Antigravity to get things done with Flutter. + +--- + +## Capabilities + +The `flutter-frontend-for-adk` skill orchestrates a structured, multi-phase workflow to construct a responsive Flutter client that interfaces with an ADK Python agent: + +1. **Workspace & Agent Discovery:** Maps the agent's purpose, sub-agent hierarchy, API endpoints, event streams, and human-in-the-loop triggers. +2. **Frontend Usage & Behavior:** Defines target platforms (mobile, web, desktop), user experience flows, and screen requirements. +3. **Frontend Architecture:** Establishes directory structure, state management using the `provider` package, and manual JSON serialization models. +4. **Frontend Design & Layout:** Configures visual styles, responsive column layouts, interactive states, and micro-animations. +5. **Scaffolding & Implementation:** Scaffolds the Flutter project, implements the API service (handling REST and Server-Sent Events/SSE streams), and builds the views. +6. **Workspace Integration:** Integrates frontend execution commands into workspace build tools (e.g., Makefiles, script runners) and updates version control settings. + +--- + +## Installation + +To make this skill available to Antigravity, you can install it either globally or locally within a specific workspace. + +### Global Installation + +To make the skill available across all your projects, copy this repository into your global Antigravity customizations directory: + +```bash +mkdir -p ~/.gemini/config/skills/flutter-frontend-for-adk +cp -r . ~/.gemini/config/skills/flutter-frontend-for-adk +``` + +### Local Project Installation + +To make the skill available only within a specific project, copy this repository into the `.agents/skills` directory at the root of that project: + +```bash +mkdir -p /path/to/your/project/.agents/skills/flutter-frontend-for-adk +cp -r . /path/to/your/project/.agents/skills/flutter-frontend-for-adk +``` + +Once copied, Antigravity will automatically discover the skill via its `SKILL.md` definition. + +--- + +## Step-by-Step Usage Example + +This section outlines how to use the skill to replace an existing web frontend with a Flutter client, using the official `deep_search` ADK sample agent as an example. + +### 1. Clone the ADK Samples Repository +Clone the official repository containing the Agent Development Kit samples to your local machine: + +```bash +git clone https://github.com/google/adk-samples.git +cd adk-samples +``` + +### 2. Open the Target Sample +Navigate to the Python version of the `deep_search` sample and open it in your workspace: + +```bash +cd python/deep_search +``` + +Ensure this directory is open in your IDE workspace where Antigravity is active. + +### 3. Remove the Existing Frontend +Prompt Antigravity to remove the React-based frontend that is bundled with the sample. This ensures a clean slate for the new Flutter application. + +You can use a prompt such as: +> "Please remove the React frontend that ships with this sample, along with any mention of it in associated documentation, configuration files, and build scripts." + +Antigravity will delete the React source files, remove web-related dependencies, and clean up workspace references. + +### 4. Generate the Flutter Frontend +Once the workspace is cleaned, instruct Antigravity to use the installed custom skill to build a new Flutter frontend. + +You can use a prompt such as: +> "Please use the `flutter-frontend-for-adk` skill to create a new, Flutter-based frontend for the agent." + +Antigravity will activate the skill and proceed through the six phases described in the [SKILL.md](file:///Users/redbrogdon/source/demos/flutter-frontend-for-adk/SKILL.md) file, prompting you for input and approval at the end of each phase before continuing to the next. diff --git a/flutter-frontend-for-adk/SKILL.md b/flutter-frontend-for-adk/SKILL.md new file mode 100644 index 0000000..5e44462 --- /dev/null +++ b/flutter-frontend-for-adk/SKILL.md @@ -0,0 +1,85 @@ +--- +name: flutter-frontend-for-adk +description: > + Orchestrates the discovery, mapping, design, and implementation of a premium + Flutter-based frontend for an Agent Development Kit (ADK) agent written with Python. +metadata: + author: Antigravity + version: 1.0.0 + requires: + bins: + - flutter + - dart +--- + +# Building a Flutter Frontend for an ADK Agent + +This skill orchestrates the end-to-end workflow of designing, specifying, and implementing a premium Flutter-based frontend for any Agent Development Kit (ADK) agent. + +## Prerequisites & External References + +Before starting, the developer or coding agent should locate ADK framework-level specifications using the following resources instead of restating them in this skill: +1. **Activate ADK Code Skill:** Ensure that `/google-agents-cli-adk-code` is active in the current session. Refer to its reference file `adk-python.md` to understand the core `Event`, `EventActions`, and state management models. +2. **Fetch Official Docs:** Retrieve the official documentation index by calling `curl https://adk.dev/llms.txt` and search for pages related to client serving, networking, or the Python API server. +3. **Inspect Local Code:** To see exact Pydantic definitions and FastAPI endpoint details directly: + * Open and inspect the local server definition inside your python environment at `.venv/lib/python*/site-packages/google/adk/cli/adk_web_server.py`. + * Open and inspect the event schemas at `.venv/lib/python*/site-packages/google/adk/events/event.py` and `event_actions.py`. + +Follow the phases below sequentially. **Do not skip ahead.** You must complete and record the findings of each phase before beginning the next. When creating an implementation plan, add a blocking task for the "Review" step in each phase that has one and wait for the user to perform a review before beginning the next phase. + +--- + +## Phase 1: Workspace & Agent Discovery +Identify the core purpose of the agent in this repository, understand its sub-agent hierarchy, map its API endpoints, event streams, and human-in-the-loop triggers, and record this information. +* **Action:** Read [references/agent_discovery.md](references/agent_discovery.md) for step-by-step discovery instructions. +* **Deliverable:** Create the [AGENT_INTERFACE_NOTES.md](AGENT_INTERFACE_NOTES.md) file in the root of the project. +* **Review:** Summarize (in a paragraph or two) what you just learned for the user. Invite them to inspect `AGENT_DISCOVERY_NOTES.md`, and do not proceed to Phase 2 until they indicate that you should do so. + +--- + +## Phase 2: Frontend Usage & Behavior +Define the high-level behavioral and functional requirements of the frontend: target platforms, screen specifications, feature details, and user experience flows. +* **Action:** Read [references/frontend_usage.md](references/frontend_usage.md) for instructions on defining app behavior. +* **Deliverable:** Create the [FRONTEND_USAGE_NOTES.md](FRONTEND_USAGE_NOTES.md) file in the root of the project. +* **Review:** Summarize (in a paragraph or two) what you just learned for the user. Invite them to inspect `FRONTEND_USAGE_NOTES.md`, and do not proceed to Phase 3 until they indicate that you should do so. + +--- + +## Phase 3: Frontend Architecture +Define the structural patterns of the application, including core folder directory layouts, manual model serializations, and state management setups. +* **Action:** Read [references/frontend_architecture.md](references/frontend_architecture.md) for instructions on defining app architecture. +* **Deliverable:** Create the [FRONTEND_ARCHITECTURE_NOTES.md](FRONTEND_ARCHITECTURE_NOTES.md) file in the root of the project. +* **Review:** Summarize (in a paragraph or two) what you just learned for the user. Invite them to inspect `FRONTEND_ARCHITECTURE_NOTES.md`, and do not proceed to Phase 4 until they indicate that you should do so. + +--- + +## Phase 4: Frontend Design & Layout +Design the visual structure, layout columns, styling guides, states, and micro-animations for the frontend application. +* **Action:** Read [references/frontend_design.md](references/frontend_design.md) for styling and UI blueprints. +* **Deliverable:** Create the [FRONTEND_DESIGN_NOTES.md](FRONTEND_DESIGN_NOTES.md) file in the root of the project. +* **Review:** Summarize (in a paragraph or two) what you just learned for the user. Invite them to inspect `FRONTEND_DESIGN_NOTES.md`, and do not proceed to Phase 5 until they indicate that you should do so. + +---- + +## Phase 5: Scaffolding & Implementation +Create and program the Flutter client application within the workspace, implementing all designs and specifications. +* **Action:** + 1. Read [references/frontend_best_practices.md](references/frontend_best_practices.md) for specific implementation patterns and best practices. + 2. Scaffold a fresh Flutter project inside a folder named `frontend/` at the root of the workspace. Do not specify an org name unless instructed. Create the app to build for only those platforms specified by the user -- if you're not sure, ask. + 3. Add the approved packages (`provider`, `flutter_markdown`, `url_launcher`, and `http`) to `frontend/pubspec.yaml`. + 4. Write the manual serialization models, the `AdkApiService` (implementing standard REST and SSE HTTP streamed response parsers), the `AgentProvider` state notifier, and the responsive views/widgets following `FRONTEND_ARCHITECTURE_NOTES.md` and `FRONTEND_DESIGN_NOTES.md`. + 5. Verify code cleanliness and correctness by running `flutter format`, `flutter analyze`, and `flutter test` inside the `frontend/` directory. +* **Deliverable:** A fully compiled and functional Flutter application located inside the `frontend/` folder. + +--- + +## Phase 6: Workspace Integration & Documentation +Integrate the frontend into the workspace build flows, update version control settings, and document the client setup for developers. +* **Action:** + 1. **Build Configuration:** Examine the codebase to determine how a developer builds and runs the backend agent (e.g. searching for `Makefile` targets, task configurations in `pyproject.toml` like Poe/Taskipy, `package.json` scripts, or custom `run.sh` / `build.sh` scripts). Update these automation tools or files to provide simple commands for developers to build, test, and launch the new Flutter frontend. + 2. **VCS Ignore Setup:** Update the project's version control ignore configurations to make sure they account for the new project and its artifacts. The Flutter frontend should have a nested `.gitignore` file that will account for most things. If the root `.gitignore` file ignores `lib/`, though, make sure to add an exclusion for Flutter's `lib` directory (`!frontend/lib`). + 3. **Documentation Update:** + * If the project has a `README.md` in the root directory, update it to reference the new frontend, detail prerequisites (such as the Flutter SDK version), and outline startup instructions. + * Update the default `frontend/README.md` file to describe the frontend client, its structure, and how to launch both the agent and the client using the build configuration tools updated in step 1. + * Look for any additional pre-existing docs that might need to be updated and do so. +* **Deliverable:** Updated workspace integration configurations, build scripts, and documentation files. diff --git a/flutter-frontend-for-adk/references/agent_discovery.md b/flutter-frontend-for-adk/references/agent_discovery.md new file mode 100644 index 0000000..7423ff0 --- /dev/null +++ b/flutter-frontend-for-adk/references/agent_discovery.md @@ -0,0 +1,94 @@ +# Agent Discovery & Mapping Reference + +This document directs the agent or developer to analyze the target ADK workspace and compile the `AGENT_INTERFACE_NOTES.md` file. + +--- + +## Phase 1: Workspace & Agent Discovery + +Before writing any frontend code, you must analyze the backend agent to understand its capabilities, state variables, and execution patterns. + +### 1. Identify Workspace Entrypoints +* Scan the root directory for documentation (`README.md`, developer specs) to grasp the core purpose of the agent. +* Locate the startup configurations, usually defined in a `Makefile` or `pyproject.toml` (e.g., look for `adk api_server`). + +### 2. Deep Dive Into Backend Agent Logic +Analyze the core agent definition file (typically `app/agent.py` or similar) to map out: +* **The Execution Timeline (Agent Authors):** + * Identify the `root_agent` (the primary coordinator that the server communicates with directly). + * Trace all sub-agents (e.g., in sequential structures, loop structures, or conditional routing trees). + * Note the exact `name` string of every agent. The agent's `name` string will be the value populated in the `author` field of incoming `Event` stream items. Construct a chronological list of these authors representing the pipeline execution flow. +* **State Keys & Custom Callbacks:** + * Identify agent-specific state keys by searching for `output_key="key_name"` parameters on all agent instantiations. + * Search the file for custom callbacks (functions containing parameters of type `ToolContext` or `CallbackContext`). + * Inspect these callback bodies for references to the session's state dictionary (e.g., `callback_context.state["key_name"]` or `tool_context.state["key_name"]`). + * Identify where these callbacks are registered (e.g., `before_agent_callback`, `after_agent_callback`, `after_model_callback`) on specific agents to understand exactly *when* these state variables are updated during the execution sequence. + +### 3. Determine Server Execution Flags +* Examine how the backend API server is run locally. +* Run the command with `--help` (e.g., `uv run adk api_server --help`) or check active configs to determine: + * Port and host configurations (default is `http://127.0.0.1:8000`). + * CORS origin settings (`--allow_origins`). + * Session and artifact persistence backends (e.g., `--session_service_uri`, `--artifact_service_uri`). + +### 4. Record Your Findings +* Create a new file in the root of the project called `AGENT_INTERFACE_NOTES.md`. +* In `AGENT_INTERFACE_NOTES.md` create the following sections: + 1. **Introduction**: a description of the agent, including its name, what its intended purpose is, and a rough description of how it is constructed. + 2. **Agent Entrypoint**: A description of the entrypoint of the agent, including the exact command used to start the agent locally. + 3. **Key Components**: A description of each sub-agent in the agent, including its name, class, and a description of what it does. + +--- + +## Phase 2: ADK API & Event Mapping + +The ADK API server automatically wraps the agent and exposes standard HTTP and streaming endpoints. You must fully understand how communication with the agent works. To do so, you will read and analyze the agent's code, and then record your findings in additional sections in `AGENT_INTERFACE_NOTES.md`. + +### 1. Understand API Endpoints +ADK-based agents expose a REST API that can be used to communicate with the agent. The API is documented in the ADK documentation, but you should also read the agent's code to fully understand how it works. Here are the key endpoints: + +* **Run Agent (Blocking):** `POST /run` + * Executes the agent and returns a complete list of events after completion. +* **Run Agent (SSE Streaming):** `POST /run_sse` + * Establishes a Server-Sent Events stream of events. Used for live chat responses and progress indicators. +* **Session Management:** `GET/POST/DELETE` endpoints under `/apps/{app_name}/users/{user_id}/sessions` to list, create, or delete user sessions. +* **Artifact Loading:** `GET /apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}` to load binary files or documents generated by the agent. + +### 2. Map Payload Schemas +The ADK API uses a specific JSON schema for requests and responses. You should thoroughly understand the schema and record your findings in `AGENT_INTERFACE_NOTES.md`. Here are the key fields: + +* **Run Request (`RunAgentRequest`):** + ```json + { + "app_name": "app", + "user_id": "default_user", + "session_id": "session-uuid", + "new_message": { + "role": "user", + "parts": [{ "text": "Research quantum computing." }] + }, + "streaming": true + } + ``` +* **Response Event:** Each SSE line starting with `data: ` contains an ADK `Event` JSON object. Map out these key fields in Dart: + * `id` (String): Unique identifier of the event. + * `author` (String): The name of the agent or "user" that generated the event. Useful for progress tracking. + * `content` (Object): Contains `parts`, which can hold `text` snippets, `functionCall` parameters (e.g., to `google_search`), `functionResponse` results, or `codeExecutionResult`. + * `actions` (Object): Holds metadata like `stateDelta` (map of state updates) and `artifactDelta` (map of updated files). + +### 3. Understand State, Artifact, & HITL/Resumability Management +The ADK provides global mechanisms for state persistence, file artifact delivery, and execution resumption. You must analyze the agent codebase to extract these specific implementations: + +* **State Keys Schema:** Map each discovered state key to its data structure. Inspect Python helper functions, dataclasses, or Pydantic models to identify if a key stores a `String`, a list of goals, or a complex nested `Map` (e.g., source mapping). +* **Artifact Creation:** Identify if the agent saves file artifacts to GCS or disk using `tool_context.save_artifact("name", part)`. Note their filenames, mime-types, and namespaces (e.g., `user:` vs session-scoped). +* **Human-in-the-Loop (HITL) & Resumability:** Locate exactly how the agent pauses and handles user approval gates: + * *Conversational Gate:* The agent stops execution, asks a question in text chat, and waits for a specific confirmation phrase (e.g., "looks good", "run it") to continue. + * *Formal API Gate:* The agent uses the `request_input` tool, calls `tool_context.request_confirmation(hint)`, or utilizes `require_confirmation` on a `FunctionTool`. + * *Resume Protocol:* If a formal gate is used, document how the client resumes the run (e.g., calling `/run_sse` with the corresponding `invocation_id` or `function_call_event_id` and the user's approval input). + +### 4. Record your Findings +* Add new sections to `AGENT_INTERFACE_NOTES.md` to record what you have learned: + 4. **API Endpoints and Routes**: List the endpoints exposed by the agent, what URL routes are available and what parameters and uses they have. + 5. **Payload Schemas**: List the data schemas used to send messages between the agent and client. Make sure to note which are used for requests vs responses. Record the schemas as JSON. + 6. **State and Artifact Management**: List the state variables and artifacts that the agent uses, and how they are managed. + 7. **Human-in-the-Loop (HITL) & Resumability**: Detail if and where the agent halts for user input or approval. Explain how the client should handle these gates (e.g., via chat text response or by calling a resume endpoint with specific invocation metadata). diff --git a/flutter-frontend-for-adk/references/frontend_architecture.md b/flutter-frontend-for-adk/references/frontend_architecture.md new file mode 100644 index 0000000..7520bc0 --- /dev/null +++ b/flutter-frontend-for-adk/references/frontend_architecture.md @@ -0,0 +1,211 @@ +# Frontend Architecture Reference + +This document directs the agent or developer to define the technical implementation, data model schemas, and folder structures of the frontend, and record them in a root-level `FRONTEND_ARCHITECTURE_NOTES.md` file. + +--- + +## Prerequisites + +> [!IMPORTANT] +> Before defining the frontend architecture, the developer or coding agent **must** verify that both `AGENT_INTERFACE_NOTES.md` and `FRONTEND_USAGE_NOTES.md` exist in the root of the workspace. If either file is missing, **stop immediately** and run Phase 1 (Discovery) and Phase 2 (Usage & Behavior) first. +> +> Refer to these files throughout: +> - Use `AGENT_INTERFACE_NOTES.md` to identify custom state keys, sub-agent execution pathways, and payload JSON models. +> - Use `FRONTEND_USAGE_NOTES.md` to map user features and UI screens to notifier capabilities and concrete widget files. + +--- + +## Instructions for Creating FRONTEND_ARCHITECTURE_NOTES.md + +You must analyze the agent interface (documented in `AGENT_INTERFACE_NOTES.md`) and the frontend behavior (documented in `FRONTEND_USAGE_NOTES.md`) and compile a technical architecture specification called `FRONTEND_ARCHITECTURE_NOTES.md` in the root of the workspace. + +This document must conform to the opinionated architectural constraints defined below, but adapt them to the specific backend agent by providing concrete Dart classes, file names, API payloads, and state properties. + +Create `FRONTEND_ARCHITECTURE_NOTES.md` containing the following sections: + +### 1. Architectural Patterns & Core Guidelines +Document the target architectural patterns, which must follow: +* **Layered separation of concerns:** + * *UI Layer:* Pure Flutter widgets observing state. + * *State Layer:* Single central `ChangeNotifier` managing connection, history lists, stream events, and active states. + * *Service Layer:* Wraps standard cross-platform `package:http` API calls (avoiding native `dart:io` or platform-specific web libraries) to manage connection lifecycles and SSE chunk processing. + * *Model Layer:* Plain Dart classes using manual JSON serialization. +* **SSE streaming parser:** Use a custom connection stream reading raw line-by-line chunks from the HTTP client streamed response. +* **Approved Packages:** List approved packages (`provider`, `flutter_markdown`, `url_launcher`, and `http`). Check with the user before suggesting any additional third-party dependencies. + +### 2. Concrete Directory Scaffold +Create a listing of all specific files and folder structures to be scaffolded under `lib/` matching the target agent's workflow. Give exact filenames instead of placeholders: +* `lib/main.dart` - Entrypoint, theme, and provider initialization. +* `lib/config.dart` - Config values containing the backend API host/port. +* `lib/models/` - List specific model files (e.g. `session.dart`, `event.dart`, and custom models derived from the agent's state structure, such as `research_goal.dart`, `source_citation.dart`). +* `lib/services/` - Specific service files (e.g. `adk_api_service.dart`). +* `lib/providers/` - Specific state notifier files (e.g. `agent_provider.dart`). +* `lib/ui/` - Specific screens and widget files matching the layout specification. + +### 3. Concrete Model Serialization Blueprints +Provide the exact Dart class declarations and manual JSON deserialization factories (`fromJson` / `toJson`) for: +* **Generic Envelope Models:** Standard ADK `Session`, `Event`, and `EventActions` envelopes. +* **Agent-Specific Custom Models:** Inspect `AGENT_INTERFACE_NOTES.md` Section 6 (State & Artifact Management) and map **every** state key listed there to an explicit typed model property in the `Session` model. For example, if the agent uses a `sources` state dictionary to track research sources to cite, define a typed `SourceCitation` class to match. Detail exactly how the `Session.fromJson` factory extracts and deserializes all of these structures. + +### 4. ChangeNotifier State & Action Map +Specify the exact instance variables and methods to be defined in the central `ChangeNotifier` to support the user experience described in `FRONTEND_USAGE_NOTES.md`: +* **State Fields:** List variable types and names (e.g. `List sessions`, `Session? activeSession`, `bool isExecuting`, `String? errorMessage`, etc.). +* **Action Methods:** List specific method names and signatures (e.g. `Future fetchSessionHistory()`, `Future deleteSession(String id)`, `Stream startAgentRun(String prompt)`, `void handleApprovalFeedback(String message)`). + +### 5. Stream Connection & Error Handling Logic +Detail how the service and state provider coordinate during streaming: +* Outline the SSE chunk reading process (decoding byte chunks, splitting lines, yielding decoded data frames). +* Specify how connection interruptions or server errors (e.g. non-200 responses) are propagated to the state provider to set `errorMessage` and transition state safely. + +--- + +## Architectural Guidelines Reference + +To assist you in formulating the concrete blueprints inside `FRONTEND_ARCHITECTURE_NOTES.md`, refer to the following standard code patterns: + +### A. Raw package:http SSE Parser Pattern +The client uses `http.Client` from `package:http` to stream and parse raw SSE chunks: +```dart +import 'dart:convert'; +import 'package:http/http.dart' as http; + +// Core connection logic: +final client = http.Client(); +final request = http.Request('POST', Uri.parse('$baseUrl/run_sse')); +request.headers['Content-Type'] = 'application/json'; +request.headers['Accept'] = 'text/event-stream'; +request.body = jsonEncode(payload); + +final streamedResponse = await client.send(request); +await for (final line in streamedResponse.stream + .transform(utf8.decoder) + .transform(const LineSplitter())) { + final trimmed = line.trim(); + if (trimmed.startsWith('data:')) { + final dataString = trimmed.substring(5).trim(); + // Parse JSON dataString into concrete Event models + } +} +``` + +### B. Standard Session and Event Model Envelopes +ADK serialization payload structures use camelCase properties: +```dart +class Event { + final String id; + final String? invocationId; + final String author; + final String? contentText; + final EventActions actions; + final bool partial; + final bool turnComplete; + + Event({ + required this.id, + this.invocationId, + required this.author, + this.contentText, + required this.actions, + required this.partial, + required this.turnComplete, + }); + + factory Event.fromJson(Map json) { + String? extractedText; + final content = json['content']; + if (content != null && content['parts'] != null) { + final List parts = content['parts']; + if (parts.isNotEmpty && parts.first['text'] != null) { + extractedText = parts.first['text'] as String; + } + } + return Event( + id: json['id'] as String, + invocationId: json['invocationId'] as String?, + author: json['author'] as String? ?? 'system', + contentText: extractedText, + actions: EventActions.fromJson(json['actions'] as Map? ?? {}), + partial: json['partial'] as bool? ?? false, + turnComplete: json['turnComplete'] as bool? ?? false, + ); + } +} + +class EventActions { + final Map stateDelta; + final bool endOfAgent; + + EventActions({ + required this.stateDelta, + required this.endOfAgent, + }); + + factory EventActions.fromJson(Map json) { + return EventActions( + stateDelta: json['stateDelta'] as Map? ?? {}, + endOfAgent: json['endOfAgent'] as bool? ?? false, + ); + } +} +``` + +### C. Standard REST Endpoints Pattern +The client uses standard `package:http` methods for pings, lists, creates, and deletes: +```dart +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class AdkApiService { + final String baseUrl; + final http.Client _client = http.Client(); + + AdkApiService({required this.baseUrl}); + + /// GET /health + Future pingServer() async { + try { + final response = await _client.get(Uri.parse('$baseUrl/health')); + return response.statusCode == 200; + } catch (_) { + return false; + } + } + + /// GET /apps/app/users/default_user/sessions + Future> getSessions() async { + final response = await _client.get( + Uri.parse('$baseUrl/apps/app/users/default_user/sessions'), + ); + if (response.statusCode != 200) { + throw Exception('Failed to fetch sessions: ${response.statusCode}'); + } + final List decodedList = jsonDecode(response.body); + return decodedList + .map((item) => Session.fromJson(item as Map)) + .toList(); + } + + /// POST /apps/app/users/default_user/sessions + Future createSession() async { + final response = await _client.post( + Uri.parse('$baseUrl/apps/app/users/default_user/sessions'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({}), // Send empty payload + ); + if (response.statusCode != 200) { + throw Exception('Failed to create session: ${response.statusCode}'); + } + return Session.fromJson(jsonDecode(response.body) as Map); + } + + /// DELETE /apps/app/users/default_user/sessions/{session_id} + Future deleteSession(String sessionId) async { + final response = await _client.delete( + Uri.parse('$baseUrl/apps/app/users/default_user/sessions/$sessionId'), + ); + if (response.statusCode != 200) { + throw Exception('Failed to delete session: ${response.statusCode}'); + } + } +} +``` diff --git a/flutter-frontend-for-adk/references/frontend_best_practices.md b/flutter-frontend-for-adk/references/frontend_best_practices.md new file mode 100644 index 0000000..bceb49e --- /dev/null +++ b/flutter-frontend-for-adk/references/frontend_best_practices.md @@ -0,0 +1,391 @@ +# Frontend Implementation Best Practices + +This document provides concrete code patterns and best practices for assembling the Flutter agent client. These guidelines address common implementation challenges, security configuration, typography, and styling. + +--- + +## 1. macOS and iOS Network Access & Security + +### macOS Sandbox Outbound Connections +When building and testing native macOS Flutter applications, the app runs within a secure sandbox environment by default. If the app needs to fetch assets (like Google Fonts) or communicate with local or remote APIs, it must be granted explicit outbound network permissions. + +#### Implementation Pattern +Locate the entitlements files inside the macOS runner directory: +* `frontend/macos/Runner/DebugProfile.entitlements` +* `frontend/macos/Runner/Release.entitlements` + +Ensure that the key `com.apple.security.network.client` is defined and set to `true` within the `` block: + +```xml + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + +``` + +### iOS App Transport Security (ATS) +Unlike macOS, iOS does not restrict outbound client network calls via sandbox entitlements files. However, Apple's **App Transport Security (ATS)** blocks insecure HTTP connections by default. If the application needs to make plaintext HTTP calls during development (e.g., connecting to a local backend agent server running on `http://127.0.0.1:8000` or `http://localhost:8000`), an exception must be configured. + +#### Implementation Pattern +Locate the `Info.plist` file inside the iOS runner directory: +* `frontend/ios/Runner/Info.plist` + +Add the `NSAppTransportSecurity` dictionary with `NSAllowsLocalNetworking` set to `true` to allow local loopback and local network HTTP connections: + +```xml +NSAppTransportSecurity + + NSAllowsLocalNetworking + + +``` + +--- + +## 2. Rich Markdown Rendering + +Agent response logs contain nested formatting, bold titles, lists, and code blocks. Plain text widgets are insufficient for displaying this nicely. Use `flutter_markdown` with customized typography rules that leverage the Outfit/Inter font system to maintain a premium feel. + +> [!IMPORTANT] +> **Stream-Safe Markdown Formatting:** Make sure that any text field or streaming response that could include markdown formatting is properly formatted and "pretty printed" in real time using `MarkdownBody` instead of standard `Text` widgets, even while the message is incomplete and streaming. + +### Implementation Pattern +Use `MarkdownBody` for rendering single message blocks and wrap it in a customized `MarkdownStyleSheet`: + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:google_fonts/google_fonts.dart'; + +Widget buildMarkdownMessage(BuildContext context, String text, Color textColor) { + return MarkdownBody( + data: text, + selectable: true, + styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + p: GoogleFonts.inter( + fontSize: 14, + height: 1.45, + color: textColor, + ), + strong: GoogleFonts.inter( + fontWeight: FontWeight.bold, + color: textColor, + ), + listBullet: GoogleFonts.inter( + fontSize: 14, + color: textColor, + ), + ), + ); +} +``` + +--- + +## 3. Dart Import Ordering & Formatting + +To ensure code cleanliness, maintain structure, and comply with standard style rules, imports must be structured according to the `directives_ordering` rule. + +### 1. Enable Linter Rule +Ensure that the `directives_ordering` rule is enabled in the project's [analysis_options.yaml](file:///Users/redbrogdon/source/flutter-adk/frontend/analysis_options.yaml): + +```yaml +linter: + rules: + directives_ordering: true +``` + +### 2. Automated Import Organizing & Formatting +Instead of manually ordering imports, use the Dart SDK's built-in tools to automate this process. Run the following command inside the `frontend/` directory to automatically sort all imports and resolve auto-fixable lint issues: + +```bash +dart fix --apply +``` + +To format all files according to standard style guidelines, run: + +```bash +dart format . +``` + +--- + +## 4. Sealed Classes & Exhaustive Pattern Matching for Lists + +Lists of messages or UI items from an agent run should be represented using Dart's `sealed` classes. Sealed classes restrict the inheritance hierarchy to the current library, enabling the analyzer to verify that `switch` statements and expressions are exhaustive (handling all possible types without needing a fallback/default clause). + +### Implementation Pattern + +1. **Define the Sealed Model Hierarchy**: + Define a sealed base class representing conversation items, and declare concrete subclasses for each specific data type (e.g., text, tool output, system event, surface widgets): + + ```dart + import 'package:flutter/widgets.dart'; + + sealed class ConversationItem {} + + class TextItem extends ConversationItem { + final String sender; + final String text; + + TextItem({required this.sender, required this.text}); + } + + class ImageItem extends ConversationItem { + final String url; + + ImageItem({required this.url}); + } + ``` + +2. **Render Using Exhaustive Switch Expressions**: + When building lists of items in a `ListView.builder`, map the items using a switch expression. This allows the compiler to enforce compile-time safety; if a new subclass of `ConversationItem` is added later, the compiler will fail to build until it is handled. + + ```dart + Widget buildConversationList(List items) { + return ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + + return switch (item) { + TextItem(sender: final sender, text: final text) => + BuildTextMessageBubble(sender: sender, text: text), + ImageItem(url: final url) => + BuildImageMessageBubble(url: url), + }; + }, + ); + } + ``` + +--- + +## 5. Scroll Position Anchoring (Guarding Viewport Yanking) + +When rendering live message feeds, logs, or agent streaming output, the chat window should auto-scroll to the bottom only if the user is already actively looking at the bottom. If the user scrolls up manually to review history, incoming data must not yank their viewport back to the bottom. + +### Implementation Pattern + +1. **Calculate Scroll Position**: + Track if the scroll position is currently at or near the bottom before laying out new items. Use a small threshold (such as `40` pixels) to account for layout variations. +2. **Conditional Animation**: + Trigger the scroll-to-bottom callback only if the active session changed (which warrants a complete view reset) or if the user is already at the bottom and new content is added. + +--- + +## 6. Timer Lifecycle and Resource Cleanup + +If the frontend performs periodic background tasks (such as polling a health endpoint, status checks, or refreshing data), these timers must be managed safely to prevent memory leaks and exceptions when widgets are unmounted from the tree. + +### Implementation Pattern + +1. **Keep a Timer Handle**: + Store the active `Timer` reference inside the state. +2. **Initialize in `initState`**: + Start the periodic loop during the widget initialization phase. +3. **Cancel in `dispose`**: + Cancel the timer in the `dispose` method. Always check if the state is still `mounted` before executing calls from background asynchronous triggers. + +```dart +import 'dart:async'; +import 'package:flutter/material.dart'; + +class StatusMonitor extends StatefulWidget { + const StatusMonitor({super.key}); + + @override + State createState() => _StatusMonitorState(); +} + +class _StatusMonitorState extends State { + Timer? _statusTimer; + + @override + void initState() { + super.initState(); + // Periodically run background check + _statusTimer = Timer.periodic(const Duration(seconds: 10), (timer) { + if (mounted) { + _runHealthCheck(); + } + }); + } + + @override + void dispose() { + // Prevent memory leak by canceling active timers + _statusTimer?.cancel(); + super.dispose(); + } + + void _runHealthCheck() { + // Perform API call + } + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} +``` + +--- + +## 7. Web & Cross-Platform Compatibility (Avoiding `dart:io` Pitfalls) + +To ensure the client runs flawlessly on all target platforms (especially Flutter Web / Chrome), code must be structured to avoid platform-specific libraries that throw runtime exceptions in browsers. + +### Avoid `dart:io` Imports and APIs +* **HttpClient**: Do not import or use `HttpClient` or `Platform` from `dart:io`. Web browsers do not support these native socket-based operations, causing immediate runtime crashes like `Unsupported operation: Platform._version` or `Unsupported operation: HttpClient`. +* **Cross-Platform Alternatives**: Always use standard cross-platform packages: + * Use `package:http` (e.g., `http.Client`) for network operations and SSE streams. + * Use `package:url_launcher` for opening links. + * Use `kIsWeb` from `package:flutter/foundation.dart` for checking if running on Web. + +### Safe Platform Checks +If platform-specific logic is required, check `kIsWeb` first. Never access properties of the `Platform` class from `dart:io` on web targets: + +```dart +import 'package:flutter/foundation.dart' show kIsWeb; +import 'dart:io' show Platform; // WARNING: Do not import/reference if building for web! + +bool get isWebOrDesktop { + if (kIsWeb) return true; + // This is safe because Platform is only accessed when kIsWeb is false + return Platform.isMacOS || Platform.isWindows || Platform.isLinux; +} +``` + +*Best Practice:* Whenever possible, use UI responsiveness thresholds (like `LayoutBuilder` width constraints) to adapt screens, rather than hardcoding platform checks. + +--- + +## 8. Server-Sent Events (SSE) Streaming vs. Completed Message Handling + +When listening to a Server-Sent Events (SSE) stream from the ADK server, the client receives both partial chunk events (representing incremental text generation) and final completed events. To prevent duplicate, fragmented, or partial message bubbles from cluttering the conversation history, follow these guidelines: + +### 1. Separate Stream States +* **Partial Event Chunk (`event.partial == true`):** The text in these events represents active streaming data. Do **NOT** add these events to the permanent session event history list. Instead, accumulate the incoming characters inside temporary state variables (e.g., `activeStreamingResponse` and `activeStreamingAuthor`) in your state manager. +* **Complete Event Chunk (`event.partial == false`):** Once a final event chunk is received, append it to the permanent session event history list and clear the temporary streaming state accumulators. + +### 2. Implementation Pattern +Inside the stream receiver block in your state provider, handle the parsed events like so: + +```dart +await for (final event in eventStream) { + if (event.partial) { + // Accumulate streaming text in temporary state variables + _activeStreamingAuthor = event.author; + _activeStreamingResponse = (_activeStreamingResponse ?? '') + (event.contentText ?? ''); + } else { + // Stream chunk finished: Clear accumulator + _activeStreamingResponse = null; + _activeStreamingAuthor = null; + + // Append the completed event to conversation history list + final updatedEvents = List.from(_activeSession!.events)..add(event); + _activeSession = _activeSession!.copyWith(events: updatedEvents); + + // Apply state deltas if present + if (event.actions.stateDelta.isNotEmpty) { + _mergeStateDelta(event.actions.stateDelta); + } + } + notifyListeners(); +} +``` + +--- + +## 9. Casing Resiliency in JSON Deserialization (ADK Framework Discrepancy) + +The ADK framework-level endpoints return camelCase keys (such as `invocationId`, `turnComplete`, `functionCall`) in `/run_sse` chunked streams, but return snake_case keys (such as `invocation_id`, `turn_complete`, `function_call`) in REST history responses. The parser must check for both casings to support all ADK-based agents. + +### Implementation Pattern + +When parsing fields that differ between streams and history, check both keys: + +```dart +final fc = part['functionCall'] ?? part['function_call']; +final id = json['id'] ?? json['id']; +final invocationId = json['invocationId'] ?? json['invocation_id']; +``` + +--- + +## 10. Generic Tool Log Extraction (Built-in Grounding vs. Explicit Function Calls) + +To ensure that the client can display the tool execution history of any ADK-based agent (including routing decisions, planning operations, or web searches), the frontend should parse all tool invocations generically rather than hardcoding checks for specific tool names (e.g. `google_search`). + +### 1. Define a Generic `ToolCall` Model + +```dart +class ToolCall { + final String name; + final Map arguments; + + ToolCall({required this.name, required this.arguments}); + + String get displayString { + if (name == 'google_search') { + final q = arguments['query'] ?? + arguments['search_query'] ?? + arguments['searchQuery'] ?? + arguments['q'] ?? + ''; + return 'Google Search: "$q"'; + } + // Format other tools nicely, e.g. "plan_generator(request: ...)" + final argsStr = arguments.entries + .map((e) => '${e.key}: ${e.value}') + .join(', '); + return '$name($argsStr)'; + } +} +``` + +### 2. Implementation Pattern + +In the `Event` deserializer, extract both internal Gemini grounding queries and explicit function calls from parts into the `toolCalls` list: + +```dart +final List calls = []; + +// 1. Extract from grounding metadata (Gemini built-in search tool) +final gm = json['groundingMetadata'] ?? json['grounding_metadata']; +if (gm != null && gm is Map) { + final wsq = gm['webSearchQueries'] ?? gm['web_search_queries']; + if (wsq != null && wsq is List) { + for (final q in wsq) { + if (q != null) { + calls.add(ToolCall( + name: 'google_search', + arguments: {'query': q.toString()}, + )); + } + } + } +} + +// 2. Extract from parts (for explicit/custom tool calls) +for (final part in partsList) { + if (part is Map) { + final fc = part['functionCall'] ?? part['function_call']; + if (fc != null && fc is Map) { + final name = fc['name'] as String? ?? 'unknown_tool'; + final args = fc['args'] != null && fc['args'] is Map + ? Map.from(fc['args'] as Map) + : {}; + calls.add(ToolCall(name: name, arguments: args)); + } + } +} +``` + diff --git a/flutter-frontend-for-adk/references/frontend_design.md b/flutter-frontend-for-adk/references/frontend_design.md new file mode 100644 index 0000000..a3321a5 --- /dev/null +++ b/flutter-frontend-for-adk/references/frontend_design.md @@ -0,0 +1,131 @@ +# Frontend Design & Layout Reference + +This document directs the agent or developer to define the visual style, theming, responsive layouts, and interactive behaviors of the frontend, and record them in a root-level `FRONTEND_DESIGN_NOTES.md` file. + +--- + +## Prerequisites + +> [!IMPORTANT] +> Before defining the frontend design, the developer or coding agent **must** verify that `AGENT_INTERFACE_NOTES.md`, `FRONTEND_USAGE_NOTES.md`, and `FRONTEND_ARCHITECTURE_NOTES.md` exist in the root of the workspace. If any of these files are missing, **stop immediately** and complete the earlier phases first. +> +> Refer to these files throughout: +> - Use `AGENT_INTERFACE_NOTES.md` to understand the target agent itself (its core purpose, sub-agent catalog, and execution lifecycle) so that you can design custom visual indicators and timeline transitions that accurately reflect the running backend workflow. +> - Use `FRONTEND_USAGE_NOTES.md` to identify the required screens, features, and conversational sequences that need design specs. +> - Use `FRONTEND_ARCHITECTURE_NOTES.md` to align screen designs and widget layout divisions with the technical file structure (e.g. matching `dashboard_screen.dart` and custom widgets). + +--- + +## Instructions for Creating FRONTEND_DESIGN_NOTES.md + +### Step 1: Collect User Design Preferences +Before writing the design notes, the coding agent **must** present a set of options to the user in the chat and ask for their preferences. Do not assume or decide on these values without user input. Ask the user for their preferences on these topics: + +1. **Visual Theme & Tone** +2. **Primary Accent Color** +3. **Typography & Font Choices** +4. **Animations & Motion Style** + +Suggest some pre-generated options for each one based on the notes in `FRONTEND_USAGE_NOTES.md`, but allow the user to specify something else. + +--- + +### Step 2: Analyze the Agent's Screens & Adapt Layouts +Read `FRONTEND_USAGE_NOTES.md` Section 3 (Screen Specifications) to identify the specific screens, panels, drawers, or views that this specific agent requires. +Do not assume a standard 3-column split view; adapt your visual layout to the specific screens defined by the user's agent. + +--- + +### Step 3: Write FRONTEND_DESIGN_NOTES.md +Create `FRONTEND_DESIGN_NOTES.md` in the root of the project containing the following sections, using the user's responses from Step 1 and the screens analyzed in Step 2: + +#### 1. Global Theme & Visual Style +Specify the exact theme configurations for the application: +* **Color Palette:** Define the HEX values and Flutter color codes (`0xFF...`) for: + * *App Background:* Main scaffold background canvas. + * *Surface Background:* Lighter background for cards, sidebars, and input overlays. + * *Accent Primary:* Vibrant highlight color for primary actions, sliders, or active progress steps (based on the user's accent preference). + * *Accent Glow/Secondary:* Auxiliary highlight color for alerts or success indicators. + * *Text Primary:* Highly legible font color for headings, text inputs, and outputs. + * *Text Secondary:* Muted font color for helper labels and timestamp metadata. + * *Borders / Dividers:* Subtle boundary lines separating layout panes. +* **Typography Hierarchy:** Outline the chosen font families, font sizes, weights, and line-heights (optimized for readability based on the user's font preferences). +* **Visual Enhancements:** Shadow parameters (`BoxShadow` values), border radii (typically `12px` for modern cards, `8px` for inputs), and overlay opacities. +* **Accessibility Design:** Define guidelines for: + * Supporting system text-scaling preferences (preventing text clipping or overflow). + * Ensuring AA/AAA contrast ratios for text on active surfaces. + * Minimum touch/tap target sizes (minimum `48x48` logical pixels for mobile/tablet platforms). + +#### 2. Screen Specifications & Route Navigation +Define the routes, navigation patterns, and panel layouts for the specific screens derived in Step 2: +* **Routing Scheme:** Specify route paths and names (e.g., `/` for Dashboard, `/settings` for settings). +* **Screen Layout Structure:** Explain how the screen real estate is shared among the agent-specific panels on widescreen/desktop layouts. + +#### 3. Screen Breakpoints & Adaptive Layouts +Detail exactly how the app reflows the agent-specific panels across different screen widths: +* **Breakpoint Definitions:** + * *Desktop / Widescreen:* Width `>= 1024px` (All major panels visible side-by-side). + * *Tablet / Medium:* Width `< 1024px` and `>= 600px` (Sidebar collapses into a drawer, panels share remaining split space). + * *Mobile:* Width `< 600px` (Single column; reflows split panels into tabbed navigation pages or bottom sheets). +* **Platform Adaptations:** Design adjustments for target platforms (e.g., scrollbars and mouse-hover states for Desktop/Web, safe area margins and touch gestures for Mobile/iOS/Android). + +#### 4. Interactive Components & Micro-animations +Define states and animations matching the user's motion preference (Step 1) for the custom widgets: +* **Active Processing Indicators:** Pulse animations or loading spinners representing running agent states. +* **Hover & Active Highlights:** Visual feedback (scale, color shifts, opacity changes) when hovering or clicking buttons, cards, and links. +* **Content Transitions:** Smooth opacity fades (`FadeIn`) or sliding animations (`SlideTransition`) for streaming content blocks, plan checklists, or new message feeds. + +--- + +## Design Reference Guidelines + +Use the following code definitions as standard implementation patterns when writing `FRONTEND_DESIGN_NOTES.md`: + +### A. Dark Mode Material ThemeData Definition +```dart +final ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF0B0F19), + cardColor: const Color(0xFF131926), + primaryColor: const Color(0xFF6366F1), + dividerColor: const Color(0xFF1E293B), + textTheme: const TextTheme( + headlineLarge: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFFF8FAFC)), + bodyMedium: TextStyle(fontSize: 15, height: 1.5, color: Color(0xFF94A3B8)), + ), +); +``` + +### B. Responsive Layout Implementation Pattern +Use `LayoutBuilder` to dynamically reflow the UI columns based on screen width: +```dart +Widget build(BuildContext context) { + return Scaffold( + body: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth >= 1024) { + // Desktop: Show sidebar and panels side-by-side + return Row( + children: [ + const SidebarWidget(width: 260), + Expanded(child: ChatPanelWidget()), + Expanded(child: WorkspacePanelWidget()), + ], + ); + } else if (constraints.maxWidth >= 600) { + // Tablet: Collapsible sidebar, side-by-side panels + return Row( + children: [ + Expanded(child: ChatPanelWidget()), + Expanded(child: WorkspacePanelWidget()), + ], + ); + } else { + // Mobile: Single panel with tab navigation + return MobileTabbedView(); + } + }, + ), + ); +} +``` diff --git a/flutter-frontend-for-adk/references/frontend_usage.md b/flutter-frontend-for-adk/references/frontend_usage.md new file mode 100644 index 0000000..bd58d0e --- /dev/null +++ b/flutter-frontend-for-adk/references/frontend_usage.md @@ -0,0 +1,55 @@ +# Frontend Usage & Behavior Reference + +This document directs the agent or developer to define the functional, behavioral, and feature requirements of the frontend and record them in a root-level `FRONTEND_USAGE_NOTES.md` file. + +--- + +## Prerequisites + +> [!IMPORTANT] +> Before defining the frontend usage and behavior, the developer or coding agent **must** verify that `AGENT_INTERFACE_NOTES.md` exists in the root of the workspace. If this file is missing, **stop immediately** and run Phase 1 (Workspace & Agent Discovery) first to analyze the agent and create it. + +## Instructions for Creating FRONTEND_USAGE_NOTES.md + +You must analyze the agent requirements (documented in `AGENT_INTERFACE_NOTES.md` and `README.md`) and compile a functional specification called `FRONTEND_USAGE_NOTES.md` in the root of the workspace. + +This document must focus strictly on **behavior, screens, and user features**, leaving out internal architecture patterns (like Riverpod or Clean Architecture) and visual design details (like specific color tokens, font files, and padding values). + +Create `FRONTEND_USAGE_NOTES.md` containing the following sections: + +### 1. Introduction & Target Platforms +* **Overview:** Briefly describe what the application does from a user's point of view (e.g., interactive chat assistant, structured report builder, data analyzer). +* **Target Platforms:** Detail which platforms the frontend supports (e.g. Flutter Web, macOS Desktop, iOS, Android). You should ask the user which ones they want the app to build for. + +### 2. User Experience Flow +Describe the high-level step-by-step lifecycle of a user session: +1. **Onboarding:** How the user starts (e.g., entering a topic, prompt, or file upload). +2. **Interactive Gates (if applicable):** How the user reviews and edits intermediate state (e.g. reviewing plans, configuring parameters, or providing approval feedback). +3. **Execution Observation:** What the user sees while the autonomous agent or workflow loop is executing (e.g. progress updates, sub-agent transitions, or active tool invocations). +4. **Output Review:** How the user interacts with the final deliverables (e.g. browsing reports, opening references/sources, exporting files, or viewing charts). + +### 3. Screen Specifications +Detail the functionality and interactive controls of each view in the application: +* **Dashboard Layout:** + * Sidebar containing session lists (creating new sessions, switching between runs, and deleting sessions). + * Connection status indicators (backend API online/offline). +* **Interaction / Chat View:** + * Standard text input and send action. + * Message log showing user vs agent/system messages. + * *Human-in-the-Loop (HITL) Controls:* Custom interactive widgets rendered when the agent requests confirmation or input (e.g. structured checklists, parameter sliders, or yes/no approval prompts). +* **Workspace / Execution View:** + * *Progress Timeline:* A progress indicator displaying the execution status, active sub-agent name (drawn from the event's `author` tag), or current node in the workflow graph. + * *Tool Invocation Logs:* A live activity log showing active tool executions (e.g., web searches, database queries, code interpreter sandboxes) including inputs and status. + * *Rich Output Viewer:* Renderers for the completed deliverables (e.g. Markdown text, comparison tables, code syntax highlighting, or file download buttons). + * *References / Citation Drawer:* Displays source documentation, grounding links, or database record cards when the user clicks inline citations/references in the output. + +### 4. User-Facing Feature Checklist +Create a prioritized list (e.g., MUST HAVE, SHOULD HAVE, COULD HAVE) of functional features: +* Multi-turn chat conversation. +* Interactive state review and approval gate controls. +* Server-Sent Events (SSE) streaming parse (word-by-word message rendering). +* Live execution progress and sub-agent timeline tracking. +* Real-time tool execution logging. +* Markdown or rich output rendering (supporting tables, links, and code). +* Clickable citation links with source detail views. +* Session history storage (listing, switching, and deleting runs). diff --git a/genkit_flutter_agentic_app/genkit_backend/package-lock.json b/genkit_flutter_agentic_app/genkit_backend/package-lock.json index 1ee2fe9..362d37c 100644 --- a/genkit_flutter_agentic_app/genkit_backend/package-lock.json +++ b/genkit_flutter_agentic_app/genkit_backend/package-lock.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/data-urls": "^3.0.4", - "genkit-cli": "^1.0.5" + "genkit-cli": "^1.26.0" } }, "node_modules/@anthropic-ai/sdk": { @@ -90,9 +90,9 @@ } }, "node_modules/@asteasolutions/zod-to-openapi": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.0.tgz", - "integrity": "sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.4.tgz", + "integrity": "sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA==", "dev": true, "license": "MIT", "dependencies": { @@ -113,21 +113,21 @@ } }, "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", "dev": true, "license": "MIT", "dependencies": { - "colorspace": "1.1.x", + "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", "cpu": [ "ppc64" ], @@ -142,9 +142,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", "cpu": [ "arm" ], @@ -159,9 +159,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", "cpu": [ "arm64" ], @@ -176,9 +176,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", "cpu": [ "x64" ], @@ -193,9 +193,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", "cpu": [ "arm64" ], @@ -210,9 +210,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", "cpu": [ "x64" ], @@ -227,9 +227,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", "cpu": [ "arm64" ], @@ -244,9 +244,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", "cpu": [ "x64" ], @@ -261,9 +261,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", "cpu": [ "arm" ], @@ -278,9 +278,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", "cpu": [ "arm64" ], @@ -295,9 +295,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", "cpu": [ "ia32" ], @@ -312,9 +312,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", "cpu": [ "loong64" ], @@ -329,9 +329,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", "cpu": [ "mips64el" ], @@ -346,9 +346,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", "cpu": [ "ppc64" ], @@ -363,9 +363,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", "cpu": [ "riscv64" ], @@ -380,9 +380,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", "cpu": [ "s390x" ], @@ -397,9 +397,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", "cpu": [ "x64" ], @@ -414,9 +414,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", "cpu": [ "arm64" ], @@ -431,9 +431,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", "cpu": [ "x64" ], @@ -448,9 +448,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", "cpu": [ "arm64" ], @@ -465,9 +465,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", "cpu": [ "x64" ], @@ -481,10 +481,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", "cpu": [ "x64" ], @@ -499,9 +516,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", "cpu": [ "arm64" ], @@ -516,9 +533,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", "cpu": [ "ia32" ], @@ -533,9 +550,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", "cpu": [ "x64" ], @@ -767,35 +784,129 @@ } }, "node_modules/@genkit-ai/telemetry-server": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@genkit-ai/telemetry-server/-/telemetry-server-1.5.0.tgz", - "integrity": "sha512-ChikRxM9o2KkNOHZZ2teBCM5ATz1HhJIk/deMPwcz8cvlQ/2ucf37utdq0uuIB4zl2QTDNWhIwrblS+ZC8lqxQ==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@genkit-ai/telemetry-server/-/telemetry-server-1.26.0.tgz", + "integrity": "sha512-w39WmPuu0ZKX7bf1hkhAy/IZttHcII5TjpvHqg1i9qoSUisdW1CQdbnTv73AsKDgCJBfKzGuz+9TMj9YAcHp4w==", "dev": true, "license": "Apache-2.0", "dependencies": { "@asteasolutions/zod-to-openapi": "^7.0.0", - "@genkit-ai/tools-common": "1.5.0", + "@genkit-ai/tools-common": "1.26.0", "@google-cloud/firestore": "^7.6.0", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.25.0", - "@opentelemetry/core": "^1.25.0", - "@opentelemetry/sdk-metrics": "^1.25.0", + "@opentelemetry/api": "~1.9.0", + "@opentelemetry/context-async-hooks": "~1.25.0", + "@opentelemetry/core": "~1.25.0", + "@opentelemetry/sdk-metrics": "~1.25.0", "@opentelemetry/sdk-node": "^0.52.0", - "@opentelemetry/sdk-trace-base": "^1.25.0", + "@opentelemetry/sdk-trace-base": "~1.25.0", "async-mutex": "^0.5.0", "express": "^4.21.0", + "lockfile": "^1.0.4", "zod": "^3.22.4" } }, + "node_modules/@genkit-ai/telemetry-server/node_modules/@opentelemetry/context-async-hooks": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz", + "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@genkit-ai/telemetry-server/node_modules/@opentelemetry/core": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", + "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@genkit-ai/telemetry-server/node_modules/@opentelemetry/resources": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz", + "integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@genkit-ai/telemetry-server/node_modules/@opentelemetry/sdk-metrics": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz", + "integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/resources": "1.25.1", + "lodash.merge": "^4.6.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@genkit-ai/telemetry-server/node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz", + "integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@genkit-ai/telemetry-server/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", + "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@genkit-ai/tools-common": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@genkit-ai/tools-common/-/tools-common-1.5.0.tgz", - "integrity": "sha512-ccLUbw9buXtaOGtZPAViMBr0jKNKUsNTPTta9eE+FMbkRqDiSvNVzV70sxmjx75fXLVkVszgGX0prlSOrV5sdg==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@genkit-ai/tools-common/-/tools-common-1.26.0.tgz", + "integrity": "sha512-L2E6tiOMUA0wSDjN04DoPtunCa2YWD9nrc7+jh+HArrOBmV6TAUg9tXOoDKb/1J3JfRUZFJ3mwXMMT9rDgUv7A==", "dev": true, "license": "Apache-2.0", "dependencies": { "@asteasolutions/zod-to-openapi": "^7.0.0", - "@trpc/server": "10.45.0", + "@inquirer/prompts": "^7.8.0", + "@trpc/server": "^10.45.2", "adm-zip": "^0.5.12", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", @@ -808,7 +919,6 @@ "express": "^4.21.0", "get-port": "5.1.1", "glob": "^10.3.12", - "inquirer": "^8.2.0", "js-yaml": "^4.1.0", "json-2-csv": "^5.5.1", "json-schema": "^0.4.0", @@ -964,9 +1074,9 @@ } }, "node_modules/@google-cloud/storage": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.16.0.tgz", - "integrity": "sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -976,7 +1086,7 @@ "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.4", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", @@ -987,193 +1097,1010 @@ "uuid": "^8.0.0" }, "engines": { - "node": ">=14" + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@google-cloud/vertexai": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.9.3.tgz", + "integrity": "sha512-35o5tIEMLW3JeFJOaaMNR2e5sq+6rpnhrF97PuAxeOm0GlqVTESKhkGj7a5B5mmJSSSU3hUfIhcQCRRsw4Ipzg==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@google/generative-ai": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz", + "integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.2.tgz", + "integrity": "sha512-nnR5nmL6lxF8YBqb6gWvEgLdLh/Fn+kvAdX5hUOnt48sNSb0riz/93ASd2E5gvanPA41X6Yp25bIfGRp1SMb2g==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@mistralai/mistralai-gcp": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai-gcp/-/mistralai-gcp-1.5.0.tgz", + "integrity": "sha512-KUv4GziIN8do4gmPe7T85gpYW1o2Q89e0hs8PQfZhFRMYz7uYPwxHyVI5UaxWlHFcmAvyxfaOAH0OuCC38Hb6g==", + "dependencies": { + "google-auth-library": "^9.11.0" + }, + "peerDependencies": { + "zod": ">= 3" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@google-cloud/storage/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" + "engines": { + "node": ">=6.6.0" } }, - "node_modules/@google-cloud/vertexai": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.9.3.tgz", - "integrity": "sha512-35o5tIEMLW3JeFJOaaMNR2e5sq+6rpnhrF97PuAxeOm0GlqVTESKhkGj7a5B5mmJSSSU3hUfIhcQCRRsw4Ipzg==", - "license": "Apache-2.0", + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", "dependencies": { - "google-auth-library": "^9.1.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=18.0.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@google/generative-ai": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz", - "integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@grpc/grpc-js": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.2.tgz", - "integrity": "sha512-nnR5nmL6lxF8YBqb6gWvEgLdLh/Fn+kvAdX5hUOnt48sNSb0riz/93ASd2E5gvanPA41X6Yp25bIfGRp1SMb2g==", - "license": "Apache-2.0", + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", "dependencies": { - "@grpc/proto-loader": "^0.7.13", - "@js-sdsl/ordered-map": "^4.4.2" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">=12.10.0" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", - "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", - "license": "Apache-2.0", + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">=6" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=12" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "node_modules/@modelcontextprotocol/sdk/node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "side-channel": "^1.1.0" }, "engines": { - "node": ">=12" + "node": ">=0.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">= 0.10" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">=12" + "node": ">= 18" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" + "url": "https://opencollective.com/express" } }, - "node_modules/@mistralai/mistralai-gcp": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai-gcp/-/mistralai-gcp-1.5.0.tgz", - "integrity": "sha512-KUv4GziIN8do4gmPe7T85gpYW1o2Q89e0hs8PQfZhFRMYz7uYPwxHyVI5UaxWlHFcmAvyxfaOAH0OuCC38Hb6g==", + "node_modules/@modelcontextprotocol/sdk/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", "dependencies": { - "google-auth-library": "^9.11.0" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "dev": true, + "license": "ISC", "peerDependencies": { - "zod": ">= 3" + "zod": "^3.25 || ^4" } }, "node_modules/@opentelemetry/api": { @@ -2132,9 +3059,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { @@ -2160,9 +3087,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { @@ -2178,11 +3105,22 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -2193,9 +3131,9 @@ } }, "node_modules/@trpc/server": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.45.0.tgz", - "integrity": "sha512-2Fwzv6nqpE0Ie/G7PeS0EVR89zLm+c1Mw7T+RAGtU807j4oaUx0zGkBXTu5u9AI+j+BYNN2GZxJcuDTAecbr1A==", + "version": "10.45.3", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.45.3.tgz", + "integrity": "sha512-CVZM42FdGwqYQxV7vSRwb//AF8REfV60S/fUnwx7xqCnnSDus0FW/UuAOW4eQJ30RQlOPy4MYU8B3dbTAx85Ew==", "dev": true, "funding": [ "https://trpc.io/sponsor" @@ -2366,14 +3304,15 @@ } }, "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", - "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, @@ -2536,9 +3475,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2562,26 +3501,10 @@ "peerDependencies": { "ajv": "^8.0.0" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, "node_modules/ansi-regex": { @@ -2678,14 +3601,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -2789,9 +3712,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2908,9 +3831,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "dev": true, "license": "MIT" }, @@ -2972,13 +3895,13 @@ } }, "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, "license": "ISC", "engines": { - "node": ">= 10" + "node": ">= 12" } }, "node_modules/cliui": { @@ -3023,14 +3946,17 @@ } }, "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" } }, "node_modules/color-convert": { @@ -3052,32 +3978,50 @@ "license": "MIT" }, "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" } }, "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" } }, "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20" + } }, "node_modules/colorette": { "version": "2.0.20", @@ -3085,17 +4029,6 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3458,9 +4391,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3471,31 +4404,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" } }, "node_modules/escalade": { @@ -3513,16 +4447,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3557,6 +4481,29 @@ "node": ">=6" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -3603,27 +4550,31 @@ "url": "https://opencollective.com/express" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "dev": true, "license": "MIT", "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" + "ip-address": "10.0.1" }, "engines": { - "node": ">=4" + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -3702,10 +4653,26 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "version": "5.5.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.7.tgz", + "integrity": "sha512-LteOsISQ2GEiDHZch6L9hB0+MLoYVLToR7xotrzU0opCICBkxOPgHAy1HxAvtxfJNXDJpgAsQN30mkrfpO2Prg==", "funding": [ { "type": "github", @@ -3715,7 +4682,9 @@ "license": "MIT", "optional": true, "dependencies": { - "strnum": "^1.1.1" + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.1.3", + "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -3774,22 +4743,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3886,9 +4839,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -3924,14 +4877,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4118,22 +5072,24 @@ } }, "node_modules/genkit-cli": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/genkit-cli/-/genkit-cli-1.5.0.tgz", - "integrity": "sha512-DCNp0AeaEahLRCgWmUAK8gvgxVCfY7mGcrIST8erTB9s2b0uGygEgnXtbSP0DJhHWypi7GsuQUtxkkDpwIvOHA==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/genkit-cli/-/genkit-cli-1.26.0.tgz", + "integrity": "sha512-DBS2YN1sqFfaXly2XVw6yFC9FsT7bYw0NdWAzve7I18GfltIU4N5Kj7wwy4NPYDFNbK1xaU0R+zZS9u1jjc2yw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@genkit-ai/telemetry-server": "1.5.0", - "@genkit-ai/tools-common": "1.5.0", + "@genkit-ai/telemetry-server": "1.26.0", + "@genkit-ai/tools-common": "1.26.0", + "@inquirer/prompts": "^7.8.0", + "@modelcontextprotocol/sdk": "^1.13.1", "axios": "^1.7.7", "colorette": "^2.0.20", "commander": "^11.1.0", "extract-zip": "^2.0.1", "get-port": "5.1.1", - "inquirer": "^8.2.0", "open": "^6.3.0", - "ora": "^5.4.1" + "ora": "^5.4.1", + "semver": "^7.7.2" }, "bin": { "genkit": "dist/bin/genkit.js" @@ -4227,9 +5183,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4240,9 +5196,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -4496,6 +5452,16 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", + "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -4691,31 +5657,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/inquirer": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", - "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^6.0.1" - }, "engines": { - "node": ">=12.0.0" + "node": ">= 12" } }, "node_modules/ipaddr.js": { @@ -4737,13 +5686,6 @@ "node": "*" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4834,6 +5776,13 @@ "node": ">=8" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4910,9 +5859,9 @@ } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -4923,9 +5872,9 @@ } }, "node_modules/json-2-csv": { - "version": "5.5.9", - "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.9.tgz", - "integrity": "sha512-l4g6GZVHrsN+5SKkpOmGNSvho+saDZwXzj/xmcO0lJAgklzwsiqy70HS5tA9djcRvBEybZ9IF6R1MDFTEsaOGQ==", + "version": "5.5.10", + "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.10.tgz", + "integrity": "sha512-Dep8wO3Fr5wNjQevO2Z8Y7yeee/nYSGRsi7q6zJDKEVHxXkXT+v21vxHmDX923UzmCXXkSo62HaTz6eTWzFLaw==", "dev": true, "license": "MIT", "dependencies": { @@ -4957,6 +5906,13 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4993,25 +5949,25 @@ } }, "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "optional": true, "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "optional": true, "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -5023,12 +5979,12 @@ "optional": true }, "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -5077,12 +6033,12 @@ "optional": true }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -5099,12 +6055,22 @@ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", "optional": true }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "node_modules/lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", "dev": true, - "license": "MIT" + "license": "ISC", + "dependencies": { + "signal-exit": "^3.0.2" + } + }, + "node_modules/lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -5359,13 +6325,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5406,11 +6372,14 @@ "license": "MIT" }, "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/negotiator": { "version": "0.6.3", @@ -5465,9 +6434,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "license": "(BSD-3-Clause OR GPL-2.0)", "optional": true, "engines": { @@ -5647,13 +6616,13 @@ } }, "node_modules/openapi3-ts": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", - "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz", + "integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==", "dev": true, "license": "MIT", "dependencies": { - "yaml": "^2.5.0" + "yaml": "^2.8.0" } }, "node_modules/ora": { @@ -5680,16 +6649,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5728,6 +6687,22 @@ "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", "license": "MIT" }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5807,6 +6782,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/proto3-json-serializer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", @@ -5838,22 +6823,22 @@ } }, "node_modules/protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", + "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" }, @@ -6113,24 +7098,57 @@ "node": ">=14" } }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, "engines": { - "node": ">=0.12.0" + "node": ">= 18" } }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "tslib": "^2.1.0" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/safe-buffer": { @@ -6170,9 +7188,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6367,16 +7385,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6509,9 +7517,9 @@ } }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz", + "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", "funding": [ { "type": "github", @@ -6685,19 +7693,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6749,13 +7744,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", - "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -6768,19 +7763,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -6980,14 +7962,14 @@ } }, "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "dev": true, "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", + "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", @@ -7110,15 +8092,18 @@ "optional": true }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { @@ -7172,6 +8157,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.24.2", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", diff --git a/genkit_flutter_agentic_app/genkit_backend/package.json b/genkit_flutter_agentic_app/genkit_backend/package.json index c1493b2..335a228 100644 --- a/genkit_flutter_agentic_app/genkit_backend/package.json +++ b/genkit_flutter_agentic_app/genkit_backend/package.json @@ -22,6 +22,6 @@ "type": "module", "devDependencies": { "@types/data-urls": "^3.0.4", - "genkit-cli": "^1.0.5" + "genkit-cli": "^1.26.0" } } diff --git a/genlatte/.gitignore b/genlatte/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/genlatte/firebase/.firebaserc b/genlatte/firebase/.firebaserc new file mode 100644 index 0000000..b05fe7e --- /dev/null +++ b/genlatte/firebase/.firebaserc @@ -0,0 +1,7 @@ +{ + "projects": { + "default": "gcdemos-26-int-dd-latteart" + }, + "targets": {}, + "etags": {} +} diff --git a/genlatte/firebase/.gitignore b/genlatte/firebase/.gitignore new file mode 100644 index 0000000..b17f631 --- /dev/null +++ b/genlatte/firebase/.gitignore @@ -0,0 +1,69 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +firebase-debug.*.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# dataconnect generated files +.dataconnect diff --git a/genlatte/firebase/CONTEXT.md b/genlatte/firebase/CONTEXT.md new file mode 100644 index 0000000..f66c93d --- /dev/null +++ b/genlatte/firebase/CONTEXT.md @@ -0,0 +1,46 @@ +# Firebase Infrastructure (`firebase/`) + +## Purpose +This directory contains the root configuration for the GenLatte Firebase project. It orchestrates the deployment and local execution of hosting, firestore rules, remote configuration, and cloud functions. + +## Detailed File Overviews + +- **`firebase.json`**: + - **Description**: The primary configuration file for the Firebase CLI. + - **Core Logic**: + - **Firestore**: Points to `firestore.rules` and `firestore.indexes.json`. + - **Functions**: Configures the `functions/` subdirectory as the source, specifies Node 24 runtime, and defines the `npm build` predeploy hook. + - **Hosting**: Configures the `public/` folder and defines a catch-all rewrite to `index.html` for single-page applications (SPAs). + - **Emulators**: Defines port mappings for local development (Auth: 9099, Firestore: 8080, Functions: 5001, etc.). + +- **`.firebaserc`**: + - **Description**: Maps project aliases (e.g., `default`, `staging`) to specific Firebase Project IDs. + +- **`firestore.rules`**: + - **Description**: Defines security and access policies for the Firestore database. + +- **`remoteconfig.json`**: + - **Description**: The default template for project-wide dynamic configuration values. + +## Subdirectories Overview + +- **`functions/`**: The core backend source code and Node.js environment (see [functions/CONTEXT.md](functions/CONTEXT.md)). + +## Dependencies/Relationships + +- **Firebase CLI**: Necessary for all interactions within this directory. +- **Google Cloud Console**: Used for managing the underlying Firestore and Cloud Run instances mapped here. + +## Usage/Exports + +### Project Initialization +To switch between project environments: +```bash +firebase use +``` + +### Local Development +To start the full emulator suite: +```bash +firebase emulators:start +``` diff --git a/genlatte/firebase/cors.json b/genlatte/firebase/cors.json new file mode 100644 index 0000000..bd9d0df --- /dev/null +++ b/genlatte/firebase/cors.json @@ -0,0 +1,7 @@ +[ + { + "origin": ["*"], + "method": ["GET"], + "maxAgeSeconds": 3600 + } +] \ No newline at end of file diff --git a/genlatte/firebase/create_users.js b/genlatte/firebase/create_users.js new file mode 100644 index 0000000..2b2841a --- /dev/null +++ b/genlatte/firebase/create_users.js @@ -0,0 +1,79 @@ +const admin = require('firebase-admin'); + +// Initialize Firebase Admin +admin.initializeApp(); + +const auth = admin.auth(); + +const branch = process.env.BRANCH_NAME; +const password = process.env.USER_PASSWORD; + +if (!branch) { + console.error('BRANCH_NAME environment variable is not set.'); + process.exit(1); +} + +if (!password) { + console.error('USER_PASSWORD environment variable is not set.'); + process.exit(1); +} + +const usersToCreate = [ + { email: `nohe+kiosk_${branch}@google.com`, claims: { kiosk: true } }, + { email: `nohe+barista_${branch}@google.com`, claims: { barista: true } }, + { email: `nohe+queue_${branch}@google.com`, claims: { queue: true } }, + { email: `nohe+recent_${branch}@google.com`, claims: { recent: true } }, + { email: `nohe+moderator_${branch}@google.com`, claims: { moderator: true } }, +]; + +async function seedUsers() { + for (const userData of usersToCreate) { + try { + let user; + try { + user = await auth.getUserByEmail(userData.email); + console.log(`User already exists: ${userData.email}`); + } catch (error) { + if (error.code === 'auth/user-not-found') { + console.log(`Creating user: ${userData.email}`); + user = await auth.createUser({ + email: userData.email, + password: password, + emailVerified: true, + }); + } else { + throw error; + } + } + + const currentClaims = user.customClaims || {}; + const newClaims = userData.claims || {}; + + const keys1 = Object.keys(currentClaims); + const keys2 = Object.keys(newClaims); + + let claimsMatch = keys1.length === keys2.length; + if (claimsMatch) { + for (const key of keys1) { + if (currentClaims[key] !== newClaims[key]) { + claimsMatch = false; + break; + } + } + } + + if (!claimsMatch) { + console.log(`Setting custom claims for ${userData.email}:`, userData.claims); + await auth.setCustomUserClaims(user.uid, userData.claims); + } else { + console.log(`Claims already up to date for ${userData.email}`); + } + } catch (error) { + console.error(`Error processing user ${userData.email}:`, error); + process.exit(1); + } + } + console.log('User seeding completed successfully.'); +} + +seedUsers(); diff --git a/genlatte/firebase/firebase.json b/genlatte/firebase/firebase.json new file mode 100644 index 0000000..6ac5f00 --- /dev/null +++ b/genlatte/firebase/firebase.json @@ -0,0 +1,66 @@ +{ + "firestore": { + "database": "DATABASE_ID_PLACEHOLDER", + "location": "nam5", + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "functions": [ + { + "source": "functions", + "codebase": "default", + "disallowLegacyRuntimeConfig": true, + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local" + ], + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run build" + ] + } + ], + "remoteconfig": { + "template": "remoteconfig.json" + }, + "hosting": { + "public": "public", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + }, + "emulators": { + "auth": { + "port": 9099 + }, + "functions": { + "port": 5001 + }, + "firestore": { + "port": 8080 + }, + "hosting": { + "port": 5000 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true, + "tasks": { + "port": 5003 + }, + "eventarc": { + "port": 9299 + } + } +} diff --git a/genlatte/firebase/firestore.indexes.json b/genlatte/firebase/firestore.indexes.json new file mode 100644 index 0000000..3e06d2f --- /dev/null +++ b/genlatte/firebase/firestore.indexes.json @@ -0,0 +1,33 @@ +{ + "indexes": [ + { + "collectionGroup": "latteOrderMetadata", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "status", + "order": "ASCENDING" + }, + { + "fieldPath": "orderSubmittedTime", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "latteOrderMetadata", + "queryScope": "COLLECTION", + "fields":[ + { + "fieldPath": "status", + "order": "ASCENDING" + }, + { + "fieldPath": "completionTime", + "order": "ASCENDING" + } + ] + } + ], + "fieldOverrides": [] +} \ No newline at end of file diff --git a/genlatte/firebase/firestore.rules b/genlatte/firebase/firestore.rules new file mode 100644 index 0000000..ff60296 --- /dev/null +++ b/genlatte/firebase/firestore.rules @@ -0,0 +1,99 @@ +rules_version='2' + +service cloud.firestore { + match /databases/{database}/documents { + function isValidSweetener(sweetener) { + return sweetener in get(/databases/$(database)/documents/latteOptions/sweeteners).data.values || sweetener == null; + } + + function isValidMilk(milk) { + return milk in get(/databases/$(database)/documents/latteOptions/milks).data.values || milk == null; + } + + function isKiosk() { + return request.auth != null && request.auth.token.kiosk == true; + } + + function isModerator() { + return request.auth != null && request.auth.token.moderator == true; + } + + function isQueue() { + return request.auth != null && request.auth.token.queue == true; + } + + function isRecent() { + return request.auth != null && request.auth.token.recent == true; + } + + function isBarista() { + return request.auth != null && request.auth.token.barista == true; + } + + function onlyHasStringName(data) { + return data.keys().hasOnly(['name', 'milk', 'sweetener', 'happyPlace', 'imageUrl']) + && data.name is string; + } + + function kioskAllowedModification(data) { + return data.keys().hasOnly(['name', 'milk', 'sweetener', 'happyPlace', 'imageUrl', 'orderNumber', 'expiresAt']) + && data.name is string && + (!('milk' in data) || isValidMilk(data.milk)) && + (!('sweetener' in data) || isValidSweetener(data.sweetener)) && + (!('happyPlace' in data) || data.happyPlace is string || data.happyPlace == null) && + (!('imageUrl' in data) || data.imageUrl is string || data.imageUrl == null); + } + + function baristaAllowedModification(data) { + return data.keys().hasOnly(['name', 'milk', 'sweetener', 'happyPlace', 'imageUrl', 'expiresAt']) && + (!('name' in data) || data.name is string) && + (!('milk' in data) || isValidMilk(data.milk)) && + (!('sweetener' in data) || isValidSweetener(data.sweetener)) && + (!('happyPlace' in data) || data.happyPlace is string || data.happyPlace == null) && + (!('imageUrl' in data) || data.imageUrl is string || data.imageUrl == null); + } + + match /latteOrders/{orderId} { + allow read: if isKiosk() || isBarista() || isModerator() || isQueue(); // prevents annonymous auth from reading + allow create: if isKiosk() && onlyHasStringName(request.resource.data); + allow delete: if isBarista(); + allow update: if (isKiosk() && kioskAllowedModification(request.resource.data)) + || (isBarista()); + } + + match /baristas/{baristaId} { + allow read: if isBarista() || isModerator(); + allow write: if isBarista(); + } + + match /latteOptions/{optionId} { + allow read: if true; + allow write: if isModerator(); + } + + match /latteImageBatches/{batchId} { + allow read: if isKiosk() || isBarista(); + allow write: if isBarista(); + } + + match /latteOrderMetadata/{orderId} { + allow read: if isKiosk() || isBarista() || isModerator() || isQueue(); + allow write: if isBarista() || isModerator(); + } + + match /recentLatteImages/{imageId} { + allow read: if isRecent(); + allow write: if false; + } + + match /machines/{machineId} { + allow read: if isBarista() || isModerator(); + allow write: if isModerator(); + } + + // Default deny for all other collections + match /{document=**} { + allow read, write: if false; + } + } +} diff --git a/genlatte/firebase/functions/.eslintrc.js.ignore b/genlatte/firebase/functions/.eslintrc.js.ignore new file mode 100644 index 0000000..0f8e2a9 --- /dev/null +++ b/genlatte/firebase/functions/.eslintrc.js.ignore @@ -0,0 +1,33 @@ +module.exports = { + root: true, + env: { + es6: true, + node: true, + }, + extends: [ + "eslint:recommended", + "plugin:import/errors", + "plugin:import/warnings", + "plugin:import/typescript", + "google", + "plugin:@typescript-eslint/recommended", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + project: ["tsconfig.json", "tsconfig.dev.json"], + sourceType: "module", + }, + ignorePatterns: [ + "/lib/**/*", // Ignore built files. + "/generated/**/*", // Ignore generated files. + ], + plugins: [ + "@typescript-eslint", + "import", + ], + rules: { + "quotes": ["error", "double"], + "import/no-unresolved": 0, + "indent": ["error", 2], + }, +}; diff --git a/genlatte/firebase/functions/.gitignore b/genlatte/firebase/functions/.gitignore new file mode 100644 index 0000000..9be0f01 --- /dev/null +++ b/genlatte/firebase/functions/.gitignore @@ -0,0 +1,10 @@ +# Compiled JavaScript files +lib/**/*.js +lib/**/*.js.map + +# TypeScript v1 declaration files +typings/ + +# Node.js dependency directory +node_modules/ +*.local \ No newline at end of file diff --git a/genlatte/firebase/functions/CONTEXT.md b/genlatte/firebase/functions/CONTEXT.md new file mode 100644 index 0000000..ee178fd --- /dev/null +++ b/genlatte/firebase/functions/CONTEXT.md @@ -0,0 +1,44 @@ +# Cloud Functions Platform (`firebase/functions/`) + +## Purpose +This directory represents the Node.js environment and build configuration for all project Cloud Functions. It provides the infrastructure necessary to develop, test (via emulators), and deploy backend logic to Firebase. + +## Detailed File Overviews + +- **`package.json`**: + - **Description**: Standard Node.js manifest file. + - **Core Logic**: + - Declares dependencies including `@google/genai` for Vertex AI interaction and `sharp` for image processing. + - Defines build scripts (`tsc`) and emulator utility commands (`serve:all`). + - Restricts the runtime engine to Node 24 for modern feature support. + +- **`tsconfig.json`**: + - **Description**: TypeScript compiler configuration, ensuring strict type checking and consistent output in the `lib/` directory. + +- **`src/`**: + - **Description**: The core source code directory (see [src/CONTEXT.md](src/CONTEXT.md)). + +- **`lib/`**: + - **Description**: Generated directory containing compiled JavaScript intended for deployment. + +## Dependencies/Relationships + +- **Firebase CLI**: Necessary for running the emulators and deploying the package. +- **Node.js 24**: The required runtime environment. +- **Shared Data**: Indirectly related to the shared project data structures maintained in cross-module packages. + +## Usage/Exports + +### Development Workflow +To compile and run local emulators: +```bash +cd firebase/functions +npm install +npm run build +npm run serve:all +``` + +Deployment is handled via the Firebase CLI: +```bash +npm run deploy +``` diff --git a/genlatte/firebase/functions/package.json b/genlatte/firebase/functions/package.json new file mode 100644 index 0000000..1f46c17 --- /dev/null +++ b/genlatte/firebase/functions/package.json @@ -0,0 +1,36 @@ +{ + "name": "functions", + "scripts": { + "lint": "eslint --ext .js,.ts .", + "build": "tsc", + "build:watch": "tsc --watch", + "serve": "npm run build && firebase emulators:start --only functions", + "serve:all": "npm run build && firebase emulators:start", + "shell": "npm run build && firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "24" + }, + "main": "lib/index.js", + "dependencies": { + "@google/genai": "^1.39.0", + "firebase-admin": "^13.6.0", + "firebase-functions": "^7.2.0", + "sharp": "^0.34.5" + }, + "devDependencies": { + "@types/sharp": "^0.31.1", + "@typescript-eslint/eslint-plugin": "^5.12.0", + "@typescript-eslint/parser": "^5.12.0", + "eslint": "^8.9.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-import": "^2.25.4", + "firebase-functions-test": "^3.4.1", + "firebase-tools": "^15.9.1", + "typescript": "^5.9.3" + }, + "private": true +} diff --git a/genlatte/firebase/functions/src/CONTEXT.md b/genlatte/firebase/functions/src/CONTEXT.md new file mode 100644 index 0000000..7004b09 --- /dev/null +++ b/genlatte/firebase/functions/src/CONTEXT.md @@ -0,0 +1,42 @@ +# Cloud Functions Source Module (`firebase/functions/src/`) + +## Purpose +This directory contains the implementation of the backend logic for GenLatte, deployed as Firebase Cloud Functions (v2). It serves as the bridge between the client-side UI and the various Google Cloud services (Firestore, Vertex AI, Cloud Tasks). + +## Detailed File Overviews + +- **`index.ts`**: + - **Description**: The main entry point and export aggregator for the entire functions package. + - **Core Logic**: + - Initializes the `firebase-admin` SDK. + - Sets global scaling options for Cloud Run (underlying Functions v2). + - Aggregates and re-exports functionality from submodules with consistent naming patterns (using `BRANCH_SUFFIX` for environment isolation). + - **Exposed Categories**: + - **Auth Triggers**: Custom claim management (`userEvents`). + - **Order Management**: Lifecycle triggers and barista actions (`ordersEvents`). + - **Generative AI**: Asynchronous image and question generation (`tasks`, `images`). + - **Infrastructure**: Moderation and printing services. + +- **`sendToPrinter.ts`**: + - **Description**: Logic for communicating with physical latte art printers. + +- **`moderation.ts`**: + - **Description**: Real-time content filtering and policy enforcement for user input. + +## Subdirectories Overview + +- **`common/`**: Shared utilities (retry logic, remote config). +- **`images/`**: High-level image generation and refinement logic. +- **`ordersEvents/`**: Core business logic for order processing. +- **`reviser/`**: AI-driven question generation for image refinement. +- **`tasks/`**: Backend workers for long-running generative AI tasks. + +## Dependencies/Relationships + +- **Firebase Admin SDK**: For cross-service interaction within GCP. +- **Vertex AI (Gemini)**: Utilized by `images` and `reviser` sub-modules for core feature logic. +- **Cloud Tasks**: Reliable task enqueuing between modules. + +## Usage/Exports + +All functions are exported from `index.ts`. For deployment, the service uses these exports to define individual Cloud Run services. diff --git a/genlatte/firebase/functions/src/common/CONTEXT.md b/genlatte/firebase/functions/src/common/CONTEXT.md new file mode 100644 index 0000000..bdfbff3 --- /dev/null +++ b/genlatte/firebase/functions/src/common/CONTEXT.md @@ -0,0 +1,34 @@ +# Common Utilities Module (`firebase/functions/src/common/`) + +## Purpose +This directory contains reusable utility functions, configuration handlers, and shared resources used across all Firebase Cloud Functions in the project. Centralizing these ensures consistency in database access, error handling, and configuration management. + +## Detailed File Overviews + +- **`retry.ts`**: + - **Description**: A generic asynchronous retry utility. + - **Core Logic**: Implements an exponential backoff strategy (`baseDelay * 2^attempt`) to gracefully handle transient failures in external services (like Generative AI APIs or database contention), with configurable retry limits. + +- **`fetchRemoteConfig.ts`**: + - **Description**: Handles dynamic configuration via Firebase Remote Config. + - **Core Logic**: Provides a type-safe way to fetch project-wide parameters such as Generative AI model names, feature flags, or threshold values, allowing for real-time application updates without redeploying code. + +- **`getDb.ts`**: + - **Description**: Centralized Firestore initialization. + - **Core Logic**: Manages the `admin.firestore()` instance, handling differences between emulator environments and production. + +- **`prompts/`**: + - **Description**: A subdirectory containing common system prompt templates used for instructing Large Language Models. + +## Dependencies/Relationships + +- **Firebase Admin SDK**: Core dependency for accessing Firestore and Remote Config. +- **Firebase Functions Logger**: Used for standardized logging of retry attempts and errors. +- **Cross-cutting**: Consumed by virtually every other module in `src/` (e.g., `images`, `ordersEvents`, `reviser`). + +## Usage/Exports + +### Shared Utilities +- `withRetry`: The primary wrapper for reliable async execution. +- `fetchRemoteConfigValues`: Method to retrieve current dynamic app settings. +- `db`: The global Firestore database instance exported for general use. diff --git a/genlatte/firebase/functions/src/common/fetchRemoteConfig.ts b/genlatte/firebase/functions/src/common/fetchRemoteConfig.ts new file mode 100644 index 0000000..7831cf7 --- /dev/null +++ b/genlatte/firebase/functions/src/common/fetchRemoteConfig.ts @@ -0,0 +1,66 @@ +import { getRemoteConfig } from "firebase-admin/remote-config"; +import { + DEFAULT_POWERUP_PROMPT, + DEFAULT_MODERATION_PROMPT, + DEFAULT_DESCRIPTION_PROMPT, + DEFAULT_QUESTION_GEN_PROMPT, + DEFAULT_SLIDER_VALUE_GEN_PROMPT, + DEFAULT_TEXT_VALUE_GEN_PROMPT +} from "./prompts"; +import { logger } from "firebase-functions/logger"; + +/** + * Fetches Remote Config values for power-up prompt and image model. + */ +export async function fetchRemoteConfigValues() { + const rc = getRemoteConfig(); + let powerupPromptInstructions = DEFAULT_POWERUP_PROMPT; + let moderationPromptInstructions = DEFAULT_MODERATION_PROMPT; + let descriptionPromptInstructions = DEFAULT_DESCRIPTION_PROMPT; + let questionGenPromptInstructions = DEFAULT_QUESTION_GEN_PROMPT; + let sliderValueGenPromptInstructions = DEFAULT_SLIDER_VALUE_GEN_PROMPT; + let textValueGenPromptInstructions = DEFAULT_TEXT_VALUE_GEN_PROMPT; + let imageModel = "gemini-3.1-flash-image-preview"; + let textModel = "gemini-3-flash-preview"; + let imageParallelism = 4; + + try { + const template = await rc.getServerTemplate({ + defaultConfig: { + "POWERUP_PROMPT_BRANCH_SUFFIX": DEFAULT_POWERUP_PROMPT, + "MODERATION_PROMPT_BRANCH_SUFFIX": DEFAULT_MODERATION_PROMPT, + "DESCRIPTION_PROMPT_BRANCH_SUFFIX": DEFAULT_DESCRIPTION_PROMPT, + "QUESTION_GEN_PROMPT_BRANCH_SUFFIX": DEFAULT_QUESTION_GEN_PROMPT, + "SLIDER_VALUE_GEN_PROMPT_BRANCH_SUFFIX": DEFAULT_SLIDER_VALUE_GEN_PROMPT, + "TEXT_VALUE_GEN_PROMPT_BRANCH_SUFFIX": DEFAULT_TEXT_VALUE_GEN_PROMPT, + "IMAGE_MODEL_BRANCH_SUFFIX": "gemini-3.1-flash-image-preview", + "TEXT_MODEL_BRANCH_SUFFIX": "gemini-3-flash-preview", + "IMAGE_PARALLELISM": 4, + }, + }); + const config = template.evaluate(); + powerupPromptInstructions = config.getString("POWERUP_PROMPT_BRANCH_SUFFIX") || powerupPromptInstructions; + moderationPromptInstructions = config.getString("MODERATION_PROMPT_BRANCH_SUFFIX") || moderationPromptInstructions; + descriptionPromptInstructions = config.getString("DESCRIPTION_PROMPT_BRANCH_SUFFIX") || descriptionPromptInstructions; + questionGenPromptInstructions = config.getString("QUESTION_GEN_PROMPT_BRANCH_SUFFIX") || questionGenPromptInstructions; + sliderValueGenPromptInstructions = config.getString("SLIDER_VALUE_GEN_PROMPT_BRANCH_SUFFIX") || sliderValueGenPromptInstructions; + textValueGenPromptInstructions = config.getString("TEXT_VALUE_GEN_PROMPT_BRANCH_SUFFIX") || textValueGenPromptInstructions; + imageModel = config.getString("IMAGE_MODEL_BRANCH_SUFFIX") || imageModel; + textModel = config.getString("TEXT_MODEL_BRANCH_SUFFIX") || textModel; + imageParallelism = config.getNumber("IMAGE_PARALLELISM") || imageParallelism; + } catch (err) { + logger.warn("Failed to fetch Remote Config, using default POWERUP_PROMPT and IMAGE_MODEL", err); + } + + return { + powerupPromptInstructions, + moderationPromptInstructions, + descriptionPromptInstructions, + questionGenPromptInstructions, + sliderValueGenPromptInstructions, + textValueGenPromptInstructions, + imageModel, + textModel, + imageParallelism + }; +} \ No newline at end of file diff --git a/genlatte/firebase/functions/src/common/getDb.ts b/genlatte/firebase/functions/src/common/getDb.ts new file mode 100644 index 0000000..dffcded --- /dev/null +++ b/genlatte/firebase/functions/src/common/getDb.ts @@ -0,0 +1,6 @@ +import { getFirestore } from "firebase-admin/firestore"; + +const DATABASE_NAME = process.env.FUNCTIONS_EMULATOR === 'true' ? '(default)' : 'DATABASE_ID_PLACEHOLDER'; +const db = getFirestore(DATABASE_NAME); + +export { db }; \ No newline at end of file diff --git a/genlatte/firebase/functions/src/common/index.ts b/genlatte/firebase/functions/src/common/index.ts new file mode 100644 index 0000000..cfbb9ed --- /dev/null +++ b/genlatte/firebase/functions/src/common/index.ts @@ -0,0 +1,3 @@ +import { withRetry } from "./retry"; +import { db } from "./getDb"; +export { withRetry, db }; \ No newline at end of file diff --git a/genlatte/firebase/functions/src/common/prompts/CONTEXT.md b/genlatte/firebase/functions/src/common/prompts/CONTEXT.md new file mode 100644 index 0000000..6c59a5c --- /dev/null +++ b/genlatte/firebase/functions/src/common/prompts/CONTEXT.md @@ -0,0 +1,16 @@ +# Common Prompts (Firebase) + +**Purpose:** +Contains fundamental LLM prompt instructions shared by various AI-driven Cloud Functions, specifically aimed at initializing image generation constraints. + +**Detailed File Overviews:** + +- `index.ts`: + - **Description**: Barrel export file. + +- `powerup.ts`: + - **Description**: The fundamental generative augmentation prompt. + - **Core Logic**: Instructs the LLM to expand a user's short input into a highly detailed, two-paragraph prompt optimized for Latte Art (e.g. telling the LLM to omit details about physical coffee cups and backgrounds, ensuring the result is constrained to just the floating pattern of a latte's crema). + +**Dependencies/Relationships:** +- Injected into prompt chains utilized by tools sitting inside `functions/src/images/` to refine user requests before sending them to the primary image generation layer. diff --git a/genlatte/firebase/functions/src/common/prompts/description.ts b/genlatte/firebase/functions/src/common/prompts/description.ts new file mode 100644 index 0000000..82ab72e --- /dev/null +++ b/genlatte/firebase/functions/src/common/prompts/description.ts @@ -0,0 +1 @@ +export const DEFAULT_DESCRIPTION_PROMPT = `Generate a one line description of this image. Keep it under 6 words at absolute most. Detail is not important -- BREVITY IS IMPORTANT.`; diff --git a/genlatte/firebase/functions/src/common/prompts/index.ts b/genlatte/firebase/functions/src/common/prompts/index.ts new file mode 100644 index 0000000..4108135 --- /dev/null +++ b/genlatte/firebase/functions/src/common/prompts/index.ts @@ -0,0 +1,15 @@ +import { DEFAULT_POWERUP_PROMPT } from "./powerup"; +import { DEFAULT_MODERATION_PROMPT } from "./moderation"; +import { DEFAULT_DESCRIPTION_PROMPT } from "./description"; +import { DEFAULT_QUESTION_GEN_PROMPT } from "./reviserQuestion"; +import { DEFAULT_SLIDER_VALUE_GEN_PROMPT } from "./reviserSlider"; +import { DEFAULT_TEXT_VALUE_GEN_PROMPT } from "./reviserText"; + +export { + DEFAULT_POWERUP_PROMPT, + DEFAULT_MODERATION_PROMPT, + DEFAULT_DESCRIPTION_PROMPT, + DEFAULT_QUESTION_GEN_PROMPT, + DEFAULT_SLIDER_VALUE_GEN_PROMPT, + DEFAULT_TEXT_VALUE_GEN_PROMPT +}; \ No newline at end of file diff --git a/genlatte/firebase/functions/src/common/prompts/moderation.ts b/genlatte/firebase/functions/src/common/prompts/moderation.ts new file mode 100644 index 0000000..fda92ab --- /dev/null +++ b/genlatte/firebase/functions/src/common/prompts/moderation.ts @@ -0,0 +1,30 @@ +export const DEFAULT_MODERATION_PROMPT = `You are an expert content moderation bot. + +**TASK** +Your goal is to determine if a given string of text is inappropriate to be used as a username or "Happy Place". + +**CRITERIA FOR INAPPROPRIATE CONTENT** +A string is considered inappropriate if it contains: +1. **Profanity:** Obscene or vulgar language. +2. **Hate Speech:** Attacks on individuals or groups based on identity. +3. **Glorification of Violence:** Promoting or celebrating violent acts. +4. **Trivialization of Tragedy:** Using historical atrocities or disasters in a trivial context (e.g., "1945 Hiroshima" as a happy place). +5. **Contemporary issues:** Referencing current news cycle items, political figures, polarizing stories, etc + +**INSTRUCTIONS** +- Analyze the input text. +- If the text meets any of the criteria above, it is NOT allowed. Set \`isAllowed\` to \`false\` and provide a \`moderationReason\`. +- If the text is acceptable, it IS allowed. Set \`isAllowed\` to \`true\` and provide an empty \`moderationReason\`. +- Your output must be valid JSON matching the specified schema. + +**EXAMPLES** + +TEXT TO MODERATE: The beach at sunset +{"isAccepted": true, "reason": null} + +TEXT TO MODERATE: [imagine something unsavory here] +{"isAccepted": false, "reason": "is unsavory"} +--- + +TEXT TO MODERATE: +`; diff --git a/genlatte/firebase/functions/src/common/prompts/powerup.ts b/genlatte/firebase/functions/src/common/prompts/powerup.ts new file mode 100644 index 0000000..2229516 --- /dev/null +++ b/genlatte/firebase/functions/src/common/prompts/powerup.ts @@ -0,0 +1,13 @@ +export const DEFAULT_POWERUP_PROMPT = ` +Give me a prompt for a photo generation model that is extremely detailed and at least two paragraphs long. +Refrain from showing any humans in the output images. +Make it a flat vector illustration with a whimsical, minimalist aesthetic. +Key characteristics include: +Geometric Simplification: The landscape elements—such as the pine trees and bushes—are reduced to clean, angular, and stylized geometric shapes rather than realistic textures. +Palette and Gradation: It utilizes a harmonious, soft-toned color palette. While the shapes are flat, the atmosphere is created through subtle linear gradients in the sky and soft, layered color blocking to suggest depth and light. +Contoured Line Work: The composition is defined by clean, consistent-weight outlines that give the characters and objects a structured, pop-up book quality. +Stylized Proportion: The subject exhibits \"chibi-adjacent\" or simplified character design, favoring clear, readable features and an expressive, cheerful personality over anatomical realism. +Graphic Balance: There is a strong emphasis on intentional negative space and clean, rhythmic curves (such as the S-curve of the path), creating a balanced, orderly, and serene visual flow. +The output image must use a sepia color palette, nothing else. There can be no other colors except sepia palette. +Photo image: +`; \ No newline at end of file diff --git a/genlatte/firebase/functions/src/common/prompts/reviserQuestion.ts b/genlatte/firebase/functions/src/common/prompts/reviserQuestion.ts new file mode 100644 index 0000000..fa8bbee --- /dev/null +++ b/genlatte/firebase/functions/src/common/prompts/reviserQuestion.ts @@ -0,0 +1,54 @@ +export const DEFAULT_QUESTION_GEN_PROMPT = ` +You are an expert prompt engineer specializing in refining user prompts for image generation models. + +Your task is to analyze an initial image and generate 2 to 4 follow-up questions to clarify the user's intent and add crucial details. + +Questions MUST be short. Eight to ten words is the maximum length for any question. Questions longer than 10 words will be rejected. Forgo any extraneous phrasing and get straight to the point. +Questions MUST be about the input image and things to change specifically in the image. Do not ask questions about things that are not in the image. +For instance, if there is a particular object in the image, you can ask about it. If there is a particular style in the image, you can ask about it. + +Your questions should help define key aspects like: +- Composition and Framing (e.g., close-up, wide shot) +- Lighting and Atmosphere (e.g., golden hour, cinematic lighting) +- Level of Detail + +NEVER ASK: +- To change the color. The output must always be Sepia. + +**Output Format:** +You must generate a single JSON array containing 2 to 4 question objects. Each object must have the following structure: +- \`question\`: (string) The text of the question you are asking the user. +- \`type\`: (string) The expected type of the answer. This must be EXACTLY ONE of: "multipleChoiceQuestion", "zeroToOneQuestion", "negativeOneToOneQuestion", or "textQuestion". + +--- +**Example:** + +**Initial User Image Description:** +A dog in a field + +**Your Output:** +[ + { + "question": "Should the scene be more or less whimsical?", + "type": "negativeOneToOneQuestion" + }, + { + "question": "What kind of dog should it be?", + "type": "textQuestion" + }, + { + "question": "What sort of lighting should there be?", + "type": "multipleChoiceQuestion" + }, + { + "question": "How much should the image be stylized?", + "type": "zeroToOneQuestion" + } +] + +**Initial User Image Description:** +{{user_supplied_image}} + +--- + +Now, generate a minumum of 2 revision questions and no more than 4 revision questions for the supplied image.`; diff --git a/genlatte/firebase/functions/src/common/prompts/reviserSlider.ts b/genlatte/firebase/functions/src/common/prompts/reviserSlider.ts new file mode 100644 index 0000000..8b8659d --- /dev/null +++ b/genlatte/firebase/functions/src/common/prompts/reviserSlider.ts @@ -0,0 +1,53 @@ +export const DEFAULT_SLIDER_VALUE_GEN_PROMPT = ` +You are an expert label maker specializing in what to set a minimum value label to and a maximum value label to based on a users question. + +Your task is to analyze the supplied user question and then generate a minimum value label and a maximum value label for the slider. + +**Output Format:** +You must generate a single JSON object containing a minimum value label and a maximum value label. The object must have the following structure: +- \`minValueLabel\`: (string) The label to display for the minimum value of the slider. +- \`maxValueLabel\`: (string) The label to display for the maximum value of the slider. + +--- +**Example:** + +User Question: "How much stylistic influence should be applied? (0 is less stylized, 1 is highly stylized)" + +Your Output: +{ + "minValueLabel": "Less Stylized", + "maxValueLabel": "Highly Stylized" +} + +User Question: "On a scale from -1 to 1, how bright should the image be? (-1 is very dark, 1 is extremely bright)" + +Your Output: +{ + "minValueLabel": "Very Dark", + "maxValueLabel": "Extremely Bright" +} + +User Question: "What should the camera angle be? (0 is ground level, 1 is bird's eye view)" + +Your Output: +{ + "minValueLabel": "Ground Level", + "maxValueLabel": "Bird's Eye View" +} + +User Question: "What level of realism do you want? (-1 for cartoon/abstract, 1 for photorealistic)" + +Your Output: +{ + "minValueLabel": "Cartoon/Abstract", + "maxValueLabel": "Photorealistic" +} + +User Question: "{{user_supplied_question}}" + +--- + +Now, generate a minimum value label and a maximum value label for the slider based on the supplied user question. + +User Question: {{userSuppliedQuestion}} +`; diff --git a/genlatte/firebase/functions/src/common/prompts/reviserText.ts b/genlatte/firebase/functions/src/common/prompts/reviserText.ts new file mode 100644 index 0000000..9ce6572 --- /dev/null +++ b/genlatte/firebase/functions/src/common/prompts/reviserText.ts @@ -0,0 +1,61 @@ +export const DEFAULT_TEXT_VALUE_GEN_PROMPT = ` +You are an expert multiple choice creator helping users pick out up to four values as answers for a +multiple choice question. + +Your task is to analyze the supplied question and generate 4 choices that a user would reasonably expect to be valid answers to the question. + +Each choice in the multiple choices you generate should be one or two words and VERY RARELY, possibly three words; but NEVER MORE. Any choices phrased in 4 or more words will be rejected. + +**Output Format:** +You must generate an array of choices that a user would reasonably expect to be valid answers to the question. + +--- +**Example:** + +User Question: "What should the camera angle be?" + +Your Output: +[ + "Ground Level", + "Bird's Eye View", + "Low Angle", + "High Angle" +] + +User Question: "What is the weather like?" + +Your Output: +[ + "Sunny", + "Rainy", + "Cloudy", + "Snowy" +] + +User Question: "What kind of lighting should be used?" + +Your Output: +[ + "Golden Hour", + "Cinematic", + "Studio", + "Neon" +] + +User Question: "What kind of environment is the scene set in?" + +Your Output: +[ + "Indoors", + "Outdoors (Nature)", + "Urban/Cityscape", + "Space/Sci-Fi" +] + +User Question: "{{user_supplied_question}}" + +--- +Now, generate 4 choices that a user would reasonably expect to be valid answers to the question. + +User Question: {{userSuppliedQuestion}} +`; diff --git a/genlatte/firebase/functions/src/common/retry.ts b/genlatte/firebase/functions/src/common/retry.ts new file mode 100644 index 0000000..7e4d306 --- /dev/null +++ b/genlatte/firebase/functions/src/common/retry.ts @@ -0,0 +1,27 @@ +import * as logger from "firebase-functions/logger"; + +/** + * Helper function to retry an async operation with exponential backoff. + */ +export async function withRetry( + fn: () => Promise, + maxRetries: number = 3, + baseDelayMs: number = 1000, + orderId?: string +): Promise { + let lastError: any; + for (let i = 0; i <= maxRetries; i++) { + try { + return await fn(); + } catch (e) { + lastError = e; + logger.error(`Error for orderId ${orderId}: ${JSON.stringify(e)}`); + if (i < maxRetries) { + const delay = baseDelayMs * Math.pow(2, i); + logger.warn(`Retry attempt ${i + 1} after error for orderId ${orderId}. Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + throw lastError; +} diff --git a/genlatte/firebase/functions/src/images/CONTEXT.md b/genlatte/firebase/functions/src/images/CONTEXT.md new file mode 100644 index 0000000..b1c299e --- /dev/null +++ b/genlatte/firebase/functions/src/images/CONTEXT.md @@ -0,0 +1,45 @@ +# Images Module (`firebase/functions/src/images/`) + +## Purpose +This directory serves as the core Generative AI image engine for the project. It handles the lifecycle of image generation from initial prompt creation to refinement based on user feedback, integrating directly with Google Cloud's Vertex AI (specifically Gemini 3 Pro models). + +## Detailed File Overviews + +- **`generateImageWithPro.ts`**: + - **Description**: A robust low-level wrapper around the Vertex AI Image generation API. + - **Core Logic**: Configures advanced generation parameters (aspect ratio, MIME type, thinking level) and includes retry logic with exponential backoff to handle transient AI service errors. + +- **`generateRevisedImages.ts`**: + - **Description**: An `onCall` function that processes user feedback (answers to generated questions) to create a new batch of refined images. + - **Core Logic**: + - Aggregates user answers and moderates text-based input. + - Calls the revised prompt generator to translate feedback into new AI prompts. + - Creates a new `latteImageBatch` in Firestore linked to the parent batch. + - Enqueues multiple `taskGenerateImage` calls via the Cloud Task queue for asynchronous generation. + +- **`generateDescriptions.ts`**: + - **Description**: Uses Multimodal LLMs to analyze generated images and provide textual descriptions. + +- **`promptsForHappyPlace.ts`**: + - **Description**: Contains the prompt engineering logic that converts a user's "Happy Place" description into a dense, detailed prompt optimized for image generation. + +- **`selectImage.ts` / `rejectImage.ts`**: + - **Description**: Handle the user's final decision on a batch. `selectImage` marks an image as the final choice for the order, while `rejectImage` marks the batch as rejected (e.g., if the user wants to try again). + +## Dependencies/Relationships + +- **Vertex AI (Gemini)**: The primary engine for both image generation and multimodal vision analysis. +- **Firebase Remote Config**: Dynamically injects model IDs and feature flags. +- **Firestore**: Tracks the state of image batches (`latteImageBatches`) and links them to orders (`latteOrderMetadata`). +- **Cloud Tasks**: Used by `generateRevisedImages.ts` to trigger asynchronous image creation without blocking the client. + +## Usage/Exports + +### Callable Functions +- `generateRevisedImages`: Triggered by the frontend when a user submits refinement answers. +- `selectImage`: Triggered when a user chooses their final latte art. +- `rejectImage`: Triggered when a user dislikes a whole batch. + +### Internal Services +- `generateImageWithPro`: Utility for raw image generation. +- `generateDescription`: Utility for analyzing image content. diff --git a/genlatte/firebase/functions/src/images/configs/CONTEXT.md b/genlatte/firebase/functions/src/images/configs/CONTEXT.md new file mode 100644 index 0000000..b811821 --- /dev/null +++ b/genlatte/firebase/functions/src/images/configs/CONTEXT.md @@ -0,0 +1,17 @@ +# Image Gen Configs (Firebase) + +**Purpose:** +Stores exact execution configuration logic blocks required when querying Gemini for generative capabilities. + +**Detailed File Overviews:** + +- `happyPlacePromptPower.ts`: + - **Description**: Generative Model config object for the Happy Place expansion call. + - **Core Logic**: Declares parameters explicitly for expanding the user's "Happy Place" input string. + - Sets `temperature: 1` aiming for highly creative variance. + - Sets `thinkingLevel: ThinkingLevel.HIGH` to employ advanced reasoning. + - Demands the AI force structured JSON outputs (`responseMimeType: 'application/json'`). + - Disables all typical `SafetySettings` blocks (`HarmBlockThreshold.BLOCK_NONE`) allowing the model leeway to respond accurately to highly diverse edge-cases. + +**Dependencies/Relationships:** +- Consumed directly when spinning up an instance of the `@google/genai` client inside Cloud Functions. diff --git a/genlatte/firebase/functions/src/images/configs/happyPlacePromptPower.ts b/genlatte/firebase/functions/src/images/configs/happyPlacePromptPower.ts new file mode 100644 index 0000000..fcbb9c8 --- /dev/null +++ b/genlatte/firebase/functions/src/images/configs/happyPlacePromptPower.ts @@ -0,0 +1,36 @@ +import { HarmBlockThreshold, HarmCategory, ThinkingLevel } from "@google/genai"; + +export const happyPlacePromptPowerGenConfig = { + maxOutputTokens: 65535, + temperature: 1, + topP: 0.95, + seed: 0, + thinkingConfig: { + thinkingLevel: ThinkingLevel.HIGH, + }, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + } + ], + responseMimeType: 'application/json', + responseSchema: { + type: "ARRAY", + items: { + type: "STRING", + }, + }, +}; \ No newline at end of file diff --git a/genlatte/firebase/functions/src/images/generateDescriptions.ts b/genlatte/firebase/functions/src/images/generateDescriptions.ts new file mode 100644 index 0000000..06615fc --- /dev/null +++ b/genlatte/firebase/functions/src/images/generateDescriptions.ts @@ -0,0 +1,41 @@ +import { logger } from "firebase-functions/logger"; +import { withRetry } from "../common"; +import { GoogleGenAI } from "@google/genai"; + +import { fetchRemoteConfigValues } from "../common/fetchRemoteConfig"; + +export const generateDescription = async (base64ImageFile: string, orderId?: string) => { + const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT; + const genAI = new GoogleGenAI({ vertexai: true, project: projectId, location: 'global' }); + const { descriptionPromptInstructions } = await fetchRemoteConfigValues(); + const prompt = [ + { + inlineData: { + mimeType: "image/jpeg", + data: base64ImageFile, + } + }, + {text: descriptionPromptInstructions} + ]; + const genConfig = { + maxOutputTokens: 20000, + temperature: 1, + topP: 0.95, + }; + return await withRetry(async () => { + const chat = genAI.chats.create({ + model: "gemini-3.1-flash-lite-preview", + config: genConfig + }); + const result = await chat.sendMessage({message:prompt}); + if(!result.text) { + logger.error("No text data returned from Gemini pro for image description"); + throw new Error("No text data returned from Gemini Pro"); + } + logger.log(`Description: ${result.text}`); + return result.text; + }, 5, 1000, orderId).catch((e) => { + logger.error(`Error generating description after retries for orderId: ${orderId}`, e); + return 'could not generate a description'; + }); +} \ No newline at end of file diff --git a/genlatte/firebase/functions/src/images/generateImageWithPro.ts b/genlatte/firebase/functions/src/images/generateImageWithPro.ts new file mode 100644 index 0000000..41c0e62 --- /dev/null +++ b/genlatte/firebase/functions/src/images/generateImageWithPro.ts @@ -0,0 +1,97 @@ +import * as logger from "firebase-functions/logger"; +import { GoogleGenAI, HarmBlockThreshold, HarmCategory, ThinkingLevel } from '@google/genai'; +import { withRetry } from "../common"; +import { fetchRemoteConfigValues } from "../common/fetchRemoteConfig"; + +/** + * Generates four images using Gemini 3 Pro (Image Preview) based on a powered-up prompt. + */ +export async function generateImageWithPro(prompt: string, orderId?: string): Promise<{image: string, prompt: string}> { + const { imageModel } = await fetchRemoteConfigValues(); + logger.log("Image model", imageModel); + logger.log(`Generating image for prompt: ${prompt}`); + const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT; + logger.log("Project ID", projectId); + const genAI = new GoogleGenAI({ vertexai: true, project: projectId, location: 'global' }); + const proConfig: any = { + maxOutputTokens: 32768, + temperature: 1, + topP: 0.95, + responseModalities: ["IMAGE"] as any, + imageConfig: { + aspectRatio: "1:1", + imageSize: "512", + outputMimeType: "image/png", + }, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, + }, + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, + } + ], + }; + if(imageModel.includes("gemini-3")) { + proConfig.thinkingConfig = { + thinkingLevel: ThinkingLevel.MINIMAL, + }; + } + + return await withRetry(async () => { + let base64Data = ""; + let mimeType = "image/png"; + + if (imageModel.includes('gemini-3')) { + const resultStream = await genAI.models.generateContentStream({ + model: imageModel, + contents: [{ role: 'user', parts: [{ text: prompt }] }], + config: proConfig + }); + for await (const chunk of resultStream) { + const candidate = chunk.candidates?.[0]; + const imagePart = candidate?.content?.parts?.find((p: any) => p.inlineData); + if (imagePart && imagePart.inlineData && imagePart.inlineData.data) { + base64Data += imagePart.inlineData.data; + mimeType = imagePart.inlineData.mimeType || mimeType; + } + } + } else { + const result = await genAI.models.generateContent({ + model: imageModel, + contents: [{ role: 'user', parts: [{ text: prompt }] }], + config: proConfig + }); + const candidate = result.candidates?.[0]; + const imagePart = candidate?.content?.parts?.find((p: any) => p.inlineData); + if (imagePart && imagePart.inlineData && imagePart.inlineData.data) { + base64Data = imagePart.inlineData.data; + mimeType = imagePart.inlineData.mimeType || mimeType; + } + } + + if (base64Data.length > 0) { + return { + image: `data:${mimeType};base64,${base64Data}`, + prompt: prompt + }; + } + throw new Error("No image data returned from Gemini Pro"); + }, 5, 1000, orderId).catch((e) => { + logger.error(`Error generating image after retries for orderId: ${orderId}`, e); + return { + image: 'could not generate an image', + prompt: prompt + }; + }); +} \ No newline at end of file diff --git a/genlatte/firebase/functions/src/images/generateRevisedImages.ts b/genlatte/firebase/functions/src/images/generateRevisedImages.ts new file mode 100644 index 0000000..3d955eb --- /dev/null +++ b/genlatte/firebase/functions/src/images/generateRevisedImages.ts @@ -0,0 +1,71 @@ +import { onCall } from "firebase-functions/https"; +import { db } from "../common"; +import { SEVEN_DAYS_MILLIS } from "../ordersEvents/common"; +import { generateRevisedImagesPrompts } from "./promptsForHappyPlace"; +import { word_moderation } from "../moderation"; +import { logger } from "firebase-functions/logger"; +import { FieldValue, Timestamp } from "firebase-admin/firestore"; +import { getFunctions } from "firebase-admin/functions"; + + + +export const generateRevisedImages = onCall({ + memory: "8GiB", + timeoutSeconds: 540, +}, async (request) => { + const { imageBatchId, imageIndex, answers } = request.data; + const existingBatchImg = await db.collection("latteImageBatches").doc(imageBatchId).get(); + const existingBatchImgData = existingBatchImg.data(); + const previousPrompt = existingBatchImgData?.[`${imageIndex}`].prompt; + const orderId = existingBatchImgData?.orderId; + logger.info(`generateRevisedImages called for orderId: ${orderId}`); + + const answersToQuestions: string = (await Promise.all(Object.entries(answers).map(async ([questionId, answerValue]: [string, any]) => { + const questionMap = existingBatchImgData?.[`${imageIndex}`].questions?.find((q: any) => { + logger.info(`${q.id} vs ${questionId}`); + return q.id === questionId; + }); + const question = questionMap?.question; + + logger.info(`Processing answer for question ${questionId}: ${question} :: ${answerValue}`); + + /// Only text questions have UCG as answers. The other question types only choose from + /// Gemini-provided options. + if (question.type === "textQuestion") { + const {moderation} = await word_moderation(answerValue, orderId); + if(!moderation.isAllowed) { + logger.log(`Answer ${answerValue} contains profane language.`); + answerValue = "Smiley Face"; + } + } + return `${question}: ${answerValue}`; + }))).join('\n'); + + const revisedPrompts = await generateRevisedImagesPrompts(previousPrompt, answersToQuestions, orderId); + + const latteImageBatchDoc = db.collection("latteImageBatches").doc(); + await latteImageBatchDoc.create({ + orderId: orderId, + createdAt: FieldValue.serverTimestamp(), + expireAt: Timestamp.fromMillis(Date.now() + SEVEN_DAYS_MILLIS), + parent: { + id: imageBatchId, + imageIndex: imageIndex + } + }); + + const latteOrderMetadataRef = db.collection("latteOrderMetadata").doc(orderId); + + await latteOrderMetadataRef.update({ + "imageBatchId": latteImageBatchDoc.id, + }); + + for(let i = 0; i < revisedPrompts.length; i++) { + getFunctions().taskQueue("taskGenerateImageBranchSuffix").enqueue({ + prompt: revisedPrompts[i], + imageBatchId: latteImageBatchDoc.id, + index: i, + isRevision: true + }); + } +}); diff --git a/genlatte/firebase/functions/src/images/index.ts b/genlatte/firebase/functions/src/images/index.ts new file mode 100644 index 0000000..49d8b69 --- /dev/null +++ b/genlatte/firebase/functions/src/images/index.ts @@ -0,0 +1,15 @@ +import { generateDescription } from "./generateDescriptions"; +import { generateImageWithPro } from "./generateImageWithPro"; +import { generateRevisedImages } from "./generateRevisedImages"; +import { generatePromptsForHappyPlace } from "./promptsForHappyPlace"; +import { rejectRevision } from "./rejectImage"; +import { selectImage } from "./selectImage"; + +export { + generateDescription, + generateImageWithPro, + generatePromptsForHappyPlace, + generateRevisedImages, + rejectRevision, + selectImage +} \ No newline at end of file diff --git a/genlatte/firebase/functions/src/images/promptsForHappyPlace.ts b/genlatte/firebase/functions/src/images/promptsForHappyPlace.ts new file mode 100644 index 0000000..fae2bd8 --- /dev/null +++ b/genlatte/firebase/functions/src/images/promptsForHappyPlace.ts @@ -0,0 +1,140 @@ +import { logger } from "firebase-functions/logger"; +import { GoogleGenAI } from "@google/genai"; +import { fetchRemoteConfigValues } from "../common/fetchRemoteConfig"; +import { withRetry } from "../common"; +import { happyPlacePromptPowerGenConfig } from "./configs/happyPlacePromptPower"; + +/** + * Enhances a user prompt for image generation using Gemini 3 Flash. + */ +async function powerUpPromptLogic(prompt: string, genAI: GoogleGenAI, powerupPromptInstructions: string, textModel: string, orderId?: string): Promise { + + const prePoweredUpPrompt = ` + ${powerupPromptInstructions} + + Please generate 4 distinct variations of the prompt based on the original prompt. + Return the response as a valid JSON array of strings. e.g. ["prompt1", "prompt2", "prompt3", "prompt4"]. + Do not include any markdown formatting or code blocks in the response, just the raw JSON. + + ORIGINAL PROMPT: + ${prompt} + + OUTPUT PROMPTS (JSON): + `; + + return await withRetry(async () => { + const chat = genAI.chats.create({ model: textModel, config: happyPlacePromptPowerGenConfig }); + const result = await chat.sendMessage({ message: prePoweredUpPrompt }); + if (!result.text) { + logger.log("No text returned from Gemini Flash"); + throw new Error("No text returned from Gemini Flash"); + } + + try { + const prompts = JSON.parse(result.text); + if (Array.isArray(prompts) && prompts.length > 0) { + // Ensure we have exactly 4 prompts, or at least return what we have + return prompts.slice(0, 4).map(p => String(p)); + } + // Fallback if not an array + return [result.text]; + } catch (e) { + logger.warn("Failed to parse JSON from powerUpPromptLogic, returning raw text as single prompt", e); + return [result.text]; + } + }, 3, 1000, orderId); +} + +export const generatePromptsForHappyPlace = async (prompt: string, orderId?: string) => { + if (!prompt || prompt === "") { + throw new Error("Prompt must not be null or empty"); + } + + try { + logger.info(`Generating prompts for prompt: ${prompt}`); + + const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT; + const genAI = new GoogleGenAI({ vertexai: true, project: projectId, location: 'global' }); + + const { powerupPromptInstructions, textModel } = await fetchRemoteConfigValues(); + + const poweredUpPrompts = await powerUpPromptLogic(prompt, genAI, powerupPromptInstructions, textModel, orderId); + + return poweredUpPrompts; + } catch (e: any) { + logger.error("Global error in generatePromptsForHappyPlace", e); + // Safely return the error to the client + return []; + } +} + +/** + * Enhances a user prompt for image generation using Gemini 3 Flash. + */ +async function powerUpRevisedPromptLogic(prompt: string, genAI: GoogleGenAI, powerupPromptInstructions: string, textModel: string, answers: string, orderId?: string): Promise { + + const prePoweredUpPrompt = ` + ${powerupPromptInstructions} + + Please generate 4 distinct variations of the prompt based on the original prompt and the instructions + to revise the image. + Return the response as a valid JSON array of strings. e.g. ["prompt1", "prompt2", "prompt3", "prompt4"]. + Do not include any markdown formatting or code blocks in the response, just the raw JSON. + + ORIGINAL PROMPT: + ${prompt} + + Instructions for revising the original image: + ${answers} + + OUTPUT PROMPTS (JSON): + `; + + return await withRetry(async () => { + const chat = genAI.chats.create({ model: textModel, config: happyPlacePromptPowerGenConfig }); + const result = await chat.sendMessage({ message: prePoweredUpPrompt }); + if (!result.text) { + logger.log("No text returned from Gemini Flash"); + throw new Error("No text returned from Gemini Flash"); + } + + try { + const prompts = JSON.parse(result.text); + if (Array.isArray(prompts) && prompts.length > 0) { + // Ensure we have exactly 4 prompts, or at least return what we have + return prompts.slice(0, 4).map(p => String(p)); + } + // Fallback if not an array + return [result.text]; + } catch (e) { + logger.warn("Failed to parse JSON from powerUpPromptLogic, returning raw text as single prompt", e); + return [result.text]; + } + }, 3, 1000, orderId); +} + +export const generateRevisedImagesPrompts = async (prompt: string, answers: string, orderId?: string) => { + if (!prompt || prompt === "") { + throw new Error("Prompt must not be null or empty"); + } + if (!answers || answers.length === 0) { + throw new Error("Answers must not be null or empty"); + } + + try { + logger.info(`Generating prompts for prompt: ${prompt}`); + + const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT; + const genAI = new GoogleGenAI({ vertexai: true, project: projectId, location: 'global' }); + + const { powerupPromptInstructions, textModel } = await fetchRemoteConfigValues(); + + const revisedPrompts = await powerUpRevisedPromptLogic(prompt, genAI, powerupPromptInstructions, textModel, answers, orderId); + + return revisedPrompts; + } catch (e: any) { + logger.error("Global error in generateRevisedImagesPrompts", e); + // Safely return the error to the client + return []; + } +} \ No newline at end of file diff --git a/genlatte/firebase/functions/src/images/rejectImage.ts b/genlatte/firebase/functions/src/images/rejectImage.ts new file mode 100644 index 0000000..881467b --- /dev/null +++ b/genlatte/firebase/functions/src/images/rejectImage.ts @@ -0,0 +1,20 @@ +import { onCall } from "firebase-functions/https"; +import { logger } from "firebase-functions/logger"; +import { db } from "../common"; + +export const rejectRevision = onCall({ + +}, async (request) => { + const { imageBatchId } = request.data; + const latteImageBatchDoc = db.collection("latteImageBatches").doc(imageBatchId); + const latteImageBatchData = await latteImageBatchDoc.get(); + const orderId = latteImageBatchData.data()?.orderId; + logger.info(`rejectRevision called for orderId: ${orderId}`); + logger.log(`Rejecting LatteImageBatch ${imageBatchId} for Order ${orderId}`); + const parentImageBatchId = latteImageBatchData.data()?.parent.id; + logger.log(`Reverting OrderId ${orderId} to LatteImageBatch ${parentImageBatchId}`); + const latteOrderMetadataRef = db.collection("latteOrderMetadata").doc(orderId); + await latteOrderMetadataRef.update({ + "imageBatchId": parentImageBatchId + }); +}) \ No newline at end of file diff --git a/genlatte/firebase/functions/src/images/selectImage.ts b/genlatte/firebase/functions/src/images/selectImage.ts new file mode 100644 index 0000000..edb7e9a --- /dev/null +++ b/genlatte/firebase/functions/src/images/selectImage.ts @@ -0,0 +1,20 @@ +import { onCall } from "firebase-functions/https"; +import { logger } from "firebase-functions/logger"; +import { db } from "../common"; + +export const selectImage = onCall({ + +}, async (request) => { + const { imageBatchId, imageIndex } = request.data; + const latteImageBatchDoc = db.collection("latteImageBatches").doc(imageBatchId); + const latteImageBatchData = await latteImageBatchDoc.get(); + const imageUrl = latteImageBatchData.data()?.[imageIndex]?.imageUrl; + const orderId = latteImageBatchData.data()?.orderId; + logger.info(`selectImage called for orderId: ${orderId}`); + const latteOrderMetadataRef = db.collection("latteOrderMetadata").doc(orderId); + await latteOrderMetadataRef.update({ + "imageUrl": imageUrl, + // Do not set status == submitted here; that is its own step + // separate from selecting an image + }); +}); \ No newline at end of file diff --git a/genlatte/firebase/functions/src/index.ts b/genlatte/firebase/functions/src/index.ts new file mode 100644 index 0000000..bbb19d0 --- /dev/null +++ b/genlatte/firebase/functions/src/index.ts @@ -0,0 +1,57 @@ +/** + * Import function triggers from their respective submodules: + * + * import {onCall} from "firebase-functions/v2/https"; + * import {onDocumentWritten} from "firebase-functions/v2/firestore"; + * + * See a full list of supported triggers at https://firebase.google.com/docs/functions + */ + +import { setGlobalOptions } from "firebase-functions"; +import * as admin from "firebase-admin"; + +// Initialize Admin SDK +admin.initializeApp(); + +setGlobalOptions({ maxInstances: 10, minInstances: 1 }); + +// Role-based custom claims +import { onUserRoleCreated, onUserRoleDeleted } from "./userEvents"; +export { onUserRoleCreated as onUserRoleCreatedBRANCH_SUFFIX }; +export { onUserRoleDeleted as onUserRoleDeletedBRANCH_SUFFIX }; + +// Order counter +import { onOrderCreated, onOrderDelete, onOrderUpdate, archiveOrders, claimOrder, completeOrder, submitOrder } from "./ordersEvents"; +export { onOrderCreated as onOrderCreatedBRANCH_SUFFIX }; +export { onOrderDelete as onOrderDeleteBRANCH_SUFFIX }; +export { onOrderUpdate as onOrderUpdateBRANCH_SUFFIX }; +export { archiveOrders as archiveOrdersBRANCH_SUFFIX }; +export { claimOrder as claimOrderBRANCH_SUFFIX }; +export { completeOrder as completeOrderBRANCH_SUFFIX }; +export { submitOrder as submitOrderBRANCH_SUFFIX }; + +// Image generation is event driven, not arbitrary + +// Moderation +import { moderation } from "./moderation"; +export { moderation as moderationBRANCH_SUFFIX }; + +// Prompt reviser +// import { revisePrompt } from "./reviser"; +// export { revisePrompt as revisePrompt_BRANCH_SUFFIX }; + +import { taskGenerateImage } from "./tasks/generateImage"; +export { taskGenerateImage as taskGenerateImageBranchSuffix }; + +import { taskGenerateQuestions } from "./tasks/generateQuestions"; +export { taskGenerateQuestions as taskGenerateQuestionsBranchSuffix }; + +import { generateRevisedImages, rejectRevision, selectImage } from "./images"; +export { + generateRevisedImages as generateRevisedImagesBranchSuffix, + rejectRevision as rejectRevisionBranchSuffix, + selectImage as selectImageBranchSuffix +}; + +import { sendToPrinter } from "./sendToPrinter"; +export { sendToPrinter as sendToPrinterBRANCH_SUFFIX }; diff --git a/genlatte/firebase/functions/src/moderation.ts b/genlatte/firebase/functions/src/moderation.ts new file mode 100644 index 0000000..1240353 --- /dev/null +++ b/genlatte/firebase/functions/src/moderation.ts @@ -0,0 +1,105 @@ +import { onCall, HttpsError } from "firebase-functions/v2/https"; +import * as logger from "firebase-functions/logger"; +import { GoogleGenAI, HarmBlockThreshold, HarmCategory, ThinkingLevel } from '@google/genai'; +import { withRetry } from "./common"; +import { fetchRemoteConfigValues } from "./common/fetchRemoteConfig"; + +export const moderation = onCall(async (request) => { + let {name} = request.data; + if (!name) { + throw new HttpsError('invalid-argument', 'Missing name'); + } + + return word_moderation(name); + +}); + +export const word_moderation = (async (word_moderation: string, orderId?: string) => { + if (!word_moderation) { + throw new Error('Missing word_moderation'); + } + + try { + logger.info(`Moderating ${word_moderation}`); + + const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT; + const genAI = new GoogleGenAI({ vertexai: true, project: projectId, location: 'global' }); + + const { textModel, moderationPromptInstructions } = await fetchRemoteConfigValues(); + + const moderation = await stringModeration(word_moderation, genAI, textModel, moderationPromptInstructions, orderId); + + return { + moderation + }; + } catch (e: any) { + logger.error("Global error in generateImages", e); + // Safely return the error to the client + throw new HttpsError("internal", "An error occurred while generating images. Please try again later.", e.message); + } + +}); + +async function stringModeration(name: any, genAI: GoogleGenAI, textModel: string, moderationPromptInstructions: string, orderId?: string) { + + const prompt = `${moderationPromptInstructions}\n${name}\n`; + + const genConfig = { + maxOutputTokens: 65535, + temperature: 1, + topP: 0.95, + seed: 0, + thinkingConfig: { + thinkingLevel: ThinkingLevel.HIGH, + }, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + } + ], + responseMimeType: 'application/json', + responseSchema: { + type: "OBJECT", + properties: { + isAllowed: { + type: "BOOLEAN", + }, + moderationReason: { + type: "STRING", + }, + }, + required: ["isAllowed", "moderationReason"], + }, + }; + + return await withRetry(async () => { + const chat = genAI.chats.create({ model: textModel, config: genConfig }); + const result = await chat.sendMessage({ message: prompt }); + if (!result.text) { + throw new Error("No result returned"); + } + + try { + return JSON.parse(result.text) as { isAllowed: boolean, moderationReason: string }; + } catch (e) { + logger.warn("Failed to parse moderation result. Falling back to default.", e); + return { + isAllowed: false, + moderationReason: "Failed to parse result", + }; + } + }, 3, 1000, orderId); +} diff --git a/genlatte/firebase/functions/src/ordersEvents/CONTEXT.md b/genlatte/firebase/functions/src/ordersEvents/CONTEXT.md new file mode 100644 index 0000000..a221b7e --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/CONTEXT.md @@ -0,0 +1,43 @@ +# Orders Events Module (`firebase/functions/src/ordersEvents/`) + +## Purpose +This directory contains the core business logic for the GenLatte order lifecycle. It manages order state transitions, metadata initialization, and backend operations triggered by either direct user actions (via Callables) or Firestore document lifecycle events (Triggers). + +## Detailed File Overviews + +- **`onOrderCreated.ts`**: + - **Description**: A Firestore trigger that fires when a new order is initialized. + - **Core Logic**: Sets the `expiresAt` timestamp for TTL-based cleanup and creates a corresponding entry in the `latteOrderMetadata` collection to track internal state and order sequencing. + +- **`onOrderUpdated.ts`**: + - **Description**: Orchestrates logic based on order status transitions. + - **Core Logic**: Monitors changes in the order state (e.g., transition to `submitted`) and performs downstream actions like notifying baristas or locking the order configuration. + +- **`submitOrder.ts`**: + - **Description**: A Callable function invoked by the kiosk when a user confirms their final order. + - **Core Logic**: Validates the selected image, locks the order against further changes, and marks it as ready in the barista queue. + +- **`claimOrder.ts` / `completeOrder.ts`**: + - **Description**: Barista-facing logic for managing the physical production of lattes. + - **Core Logic**: `claimOrder` assigns a barista to a specific request, and `completeOrder` marks the physical preparation as finished, allowing the order to move to the queue for pickup. + +- **`archiveOrders.ts`**: + - **Description**: Handles cleanup and historical archiving of finished or expired orders. + +## Dependencies/Relationships + +- **Firestore**: The primary source of truth for all order states. Logic extensively uses Firestore Transactions to ensure data consistency during claiming and completion. +- **Common Module**: Uses `./common.ts` for database naming conventions and TTL constants. +- **GenLatte Data Models**: Follows the schema defined in the shared data package to ensure consistency between frontend and backend. + +## Usage/Exports + +### Firestore Triggers +- `onOrderCreated`: Lifecycle hook for new records. +- `onOrderUpdated`: Lifecycle hook for state transitions. +- `onOrderDeleted`: Lifecycle hook for cleanup. + +### Callable Functions +- `submitOrder`: Finalizes customer ordering. +- `claimOrder`: Barista assignment. +- `completeOrder`: Finalizing preparation. diff --git a/genlatte/firebase/functions/src/ordersEvents/archiveOrders.ts b/genlatte/firebase/functions/src/ordersEvents/archiveOrders.ts new file mode 100644 index 0000000..29fe1da --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/archiveOrders.ts @@ -0,0 +1,35 @@ +import { onSchedule } from "firebase-functions/v2/scheduler"; +import { logger } from "firebase-functions/logger"; +import { db } from "../common"; + +export const archiveOrders = onSchedule("every 1 hours", async (event) => { + // Archival threshold. + const maxAgeInHours = 4; + const cutoff = new Date(Date.now() - maxAgeInHours * 60 * 60 * 1000); + + const snapshot = await db.collection("latteOrderMetadata") + .where("status", "==", "completed") + .where("completionTime", "<", cutoff) + .get(); + + if (snapshot.empty) { + logger.info("Did not find any new orders to archive"); + return; + } + + const docs = snapshot.docs; + const chunkSize = 500; + + for (let i = 0; i < docs.length; i += chunkSize) { + const batch = db.batch(); + const chunk = docs.slice(i, i + chunkSize); + + chunk.forEach((doc) => { + batch.update(doc.ref, { status: "archived" }); + }); + + await batch.commit(); + } + + logger.info(`Archived ${docs.length} orders`); +}); diff --git a/genlatte/firebase/functions/src/ordersEvents/claimOrder.ts b/genlatte/firebase/functions/src/ordersEvents/claimOrder.ts new file mode 100644 index 0000000..d092d57 --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/claimOrder.ts @@ -0,0 +1,33 @@ +import { onCall, HttpsError } from "firebase-functions/https"; +import { db } from "../common"; +import { logger } from "firebase-functions/logger"; + +export const claimOrder = onCall({ + +}, async (request) => { + const { orderId, baristaId } = request.data; + logger.info(`claimOrder called with orderId: ${orderId}`); + + if (!orderId || !baristaId) { + throw new HttpsError( + "invalid-argument", + "The 'orderId' and 'baristaId' arguments are required." + ); + } + + const latteOrderMetadataRef = db.collection("latteOrderMetadata").doc(orderId); + const docSnap = await latteOrderMetadataRef.get(); + + if (!docSnap.exists) { + throw new HttpsError("not-found", "Order not found."); + } + + if (docSnap.data()?.baristaId !== null) { + throw new HttpsError("failed-precondition", "Order is already claimed."); + } + + await latteOrderMetadataRef.update({ + status: "inProgress", + baristaId: baristaId, + }); +}); diff --git a/genlatte/firebase/functions/src/ordersEvents/common.ts b/genlatte/firebase/functions/src/ordersEvents/common.ts new file mode 100644 index 0000000..5059837 --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/common.ts @@ -0,0 +1,2 @@ +export const DATABASE_NAME = process.env.FUNCTIONS_EMULATOR === 'true' ? '(default)' : 'DATABASE_ID_PLACEHOLDER'; +export const SEVEN_DAYS_MILLIS = 7 * 24 * 60 * 60 * 1000; diff --git a/genlatte/firebase/functions/src/ordersEvents/completeOrder.ts b/genlatte/firebase/functions/src/ordersEvents/completeOrder.ts new file mode 100644 index 0000000..d9bb1ed --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/completeOrder.ts @@ -0,0 +1,114 @@ +import { FieldValue, Timestamp } from "firebase-admin/firestore"; +import { onCall, HttpsError } from "firebase-functions/https"; +import { logger } from "firebase-functions/logger"; +import { db } from "../common"; +import { SEVEN_DAYS_MILLIS } from "./common"; + +export const completeOrder = onCall({ + +}, async (request) => { + const { orderId, baristaId } = request.data; + logger.info(`completeOrder called with orderId: ${orderId}`); + + if (!orderId || !baristaId) { + throw new HttpsError( + "invalid-argument", + "The 'orderId' and 'baristaId' arguments are required." + ); + } + + const latteOrderRef = db.collection("latteOrders").doc(orderId); + const latteOrderMetadataRef = db.collection("latteOrderMetadata").doc(orderId); + + const orderDocSnap = await latteOrderRef.get(); + const metadataDocSnap = await latteOrderMetadataRef.get(); + + if (!metadataDocSnap.exists || !orderDocSnap.exists) { + throw new HttpsError("not-found", "Order not found."); + } + + const metadata = metadataDocSnap.data(); + const orderData = orderDocSnap.data(); + + if (!metadata) { + throw new HttpsError( + "not-found", + `Order metadata for ${orderId} not found.` + ); + } + + if (!orderData) { + throw new HttpsError( + "not-found", + `Order data for ${orderId} not found.` + ); + } + + if (baristaId != "moderator" && metadata.baristaId !== baristaId) { + throw new HttpsError( + "permission-denied", + `The provided baristaId ${baristaId} does not match the assigned barista for order ${orderId}.` + ); + } + + await latteOrderMetadataRef.update({ + status: "completed", + completionTime: FieldValue.serverTimestamp(), + }); + + await pushToRecentLatteImages(orderId, orderData, metadata); +}); + +/** + * Adds a completed latte order's image to the public recentLatteImages gallery + * if it has been approved. + */ +async function pushToRecentLatteImages( + orderId: string, + orderData: any, + metadata: any +) { + const imageBatchRef = db.collection("latteImageBatches").doc(metadata.imageBatchId); + const imageBatchSnap = await imageBatchRef.get(); + const imageBatchData = imageBatchSnap.data(); + + if (!imageBatchSnap.exists || !imageBatchData) { + logger.error( + `Image batch for ${orderId} not found. Cannot send to recentLatteImages. Also, wtf?` + ); + return; + } + + if (metadata.isImageApproved !== true) { + logger.error( + `Image for ${orderId} not approved. Not sending to recentLatteImages.` + ); + return; + } + + if (metadata.isNameApproved !== true) { + logger.error( + `Name for ${orderId} not approved. Not sending to recentLatteImages.` + ); + return; + } + + const fields = ["image0", "image1", "image2", "image3"]; + for (const field of fields) { + const image = imageBatchData[field]; + if (image.imageUrl === metadata.imageUrl) { + await db.collection("recentLatteImages").add({ + imageUrl: image.imageUrl, + prompt: image.prompt, + name: orderData.name, + happyPlace: orderData.happyPlace, + description: image.description, + createdAt: FieldValue.serverTimestamp(), + expireAt: Timestamp.fromMillis(Date.now() + SEVEN_DAYS_MILLIS), + }); + logger.info(`Order ${orderId} completed and image added to recentLatteImages`); + break; + } + } +} + diff --git a/genlatte/firebase/functions/src/ordersEvents/index.ts b/genlatte/firebase/functions/src/ordersEvents/index.ts new file mode 100644 index 0000000..cc30eb3 --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/index.ts @@ -0,0 +1,9 @@ +import { archiveOrders } from "./archiveOrders"; +import { claimOrder } from "./claimOrder"; +import { completeOrder } from "./completeOrder"; +import { onOrderCreated } from "./onOrderCreated"; +import { onOrderDelete } from "./onOrderDeleted"; +import { onOrderUpdate } from "./onOrderUpdated"; +import { submitOrder } from "./submitOrder"; + +export { onOrderCreated, onOrderDelete, onOrderUpdate, archiveOrders, claimOrder, completeOrder, submitOrder }; \ No newline at end of file diff --git a/genlatte/firebase/functions/src/ordersEvents/onOrderCreated.ts b/genlatte/firebase/functions/src/ordersEvents/onOrderCreated.ts new file mode 100644 index 0000000..c8005b1 --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/onOrderCreated.ts @@ -0,0 +1,49 @@ +import { getFirestore, Timestamp } from "firebase-admin/firestore"; +import { onDocumentCreated } from "firebase-functions/firestore"; +import { logger } from "firebase-functions/logger"; +import { DATABASE_NAME, SEVEN_DAYS_MILLIS } from "./common"; + +/** + * Triggered when a new order is created. + * Increments a daily counter and assigns an orderNumber to the order. + */ +export const onOrderCreated = onDocumentCreated( + { + document: "latteOrders/{orderId}", + database: DATABASE_NAME, + }, + async (event) => { + logger.info(`onOrderCreated called with orderId: ${event.params.orderId}`); + const db = getFirestore(DATABASE_NAME); + const orderRef = event.data?.ref; + + if (!orderRef) { + logger.error("No order reference found in event data."); + return; + } + + const orderRefId = event.data?.id; + if (!orderRefId) { + logger.error("No order reference ID found in event data."); + return; + } + + orderRef.update({ + expiresAt: Timestamp.fromMillis(Date.now() + SEVEN_DAYS_MILLIS) + }); + + const latteOrderMetadataRef = db.collection("latteOrderMetadata").doc(orderRefId); + + try { + await db.runTransaction(async (transaction) => { + transaction.create(latteOrderMetadataRef, { + expireAt: Timestamp.fromMillis(Date.now() + SEVEN_DAYS_MILLIS) + }); + }); + + logger.info( + `Successfully created latte order metadata for ${event.params.orderId} `); + } catch (error) { + logger.error(`Error processing order ${event.params.orderId}:`, error); + } + }); \ No newline at end of file diff --git a/genlatte/firebase/functions/src/ordersEvents/onOrderDeleted.ts b/genlatte/firebase/functions/src/ordersEvents/onOrderDeleted.ts new file mode 100644 index 0000000..a81a298 --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/onOrderDeleted.ts @@ -0,0 +1,30 @@ +import { getFirestore } from "firebase-admin/firestore"; +import { logger } from "firebase-functions/logger"; +import { onDocumentDeleted } from "firebase-functions/firestore"; +import { DATABASE_NAME } from "./common"; + +export const onOrderDelete = onDocumentDeleted( + { + document: "latteOrders/{orderId}", + database: DATABASE_NAME, + }, + async (event) => { + logger.info(`onOrderDelete called with orderId: ${event.params.orderId}`); + const db = getFirestore(DATABASE_NAME); + const orderId = event.params.orderId; + + console.log(`Document ${orderId} deleted from latteOrders. Cleaning up latteOrderMetadata...`); + + try { + // Reference the corresponding document in the other collection + const targetRef = db.collection("latteOrderMetadata").doc(orderId); + + // Perform the delete operation + await targetRef.delete(); + + console.log(`Successfully deleted matching document ${orderId} from latteOrderMetadata.`); + } catch (error) { + console.error(`Error deleting document ${orderId} from latteOrderMetadata:`, error); + } + +}); \ No newline at end of file diff --git a/genlatte/firebase/functions/src/ordersEvents/onOrderUpdated.ts b/genlatte/firebase/functions/src/ordersEvents/onOrderUpdated.ts new file mode 100644 index 0000000..59b7933 --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/onOrderUpdated.ts @@ -0,0 +1,93 @@ +import { FieldValue, Firestore, getFirestore, Timestamp } from "firebase-admin/firestore"; +import { onDocumentUpdated } from "firebase-functions/v2/firestore"; +import { logger } from "firebase-functions/logger"; +import { DATABASE_NAME, SEVEN_DAYS_MILLIS } from "./common"; +import { word_moderation } from "../moderation"; +import { generatePromptsForHappyPlace } from "../images"; +import { getFunctions } from "firebase-admin/functions"; + +export const onOrderUpdate = onDocumentUpdated( + { + document: "latteOrders/{orderId}", + database: DATABASE_NAME, + memory: "1GiB", + cpu: 1, + concurrency: 2, + minInstances: 1, + timeoutSeconds: 540, + }, + async (event) => { + logger.info(`onOrderUpdate called with orderId: ${event.params.orderId}`); + const db = getFirestore(DATABASE_NAME); + const orderBeforeUpdate = event.data?.before; + const orderAfterUpdate = event.data?.after; + + if(!orderBeforeUpdate?.exists){ + logger.info("Order created. Runs on create command listed above."); + return; + } + + if(!orderAfterUpdate){ + logger.info("Order deleted."); + db.collection("latteOrderMetadata").doc(event.params.orderId).delete(); + return; + } + + if (!orderAfterUpdate.exists) { + logger.error("No order reference found in event data."); + return; + } + + const latteOrderMetadataRef = db.collection("latteOrderMetadata").doc(orderAfterUpdate.ref.id); + if(orderBeforeUpdate?.data().happyPlace !== orderAfterUpdate.data().happyPlace){ + logger.info("Happy place updated."); + await latteOrderMetadataRef.update({ + isHappyPlaceApproved: null, + happyPlaceModerationReason: null + }); + } + const oldHappyPlace = orderBeforeUpdate?.data().happyPlace; + const happyPlace = orderAfterUpdate.data().happyPlace; + + if(happyPlace && happyPlace !== oldHappyPlace) { + const { generatePromptRequest, latteImageBatchDoc } = await createImageBatchDoc(db, happyPlace as string, event.params.orderId, orderAfterUpdate.ref.id); + for(let i = 0; i < generatePromptRequest.length; i++) { + getFunctions().taskQueue("taskGenerateImageBranchSuffix").enqueue({ + prompt: generatePromptRequest[i], + imageBatchId: latteImageBatchDoc.id, + index: i + }); + } + const {moderation} = await word_moderation(happyPlace as string, event.params.orderId); + + // We check whether the happy place has been changed under our hands. + // If so, we don't want to update the imageBatchId. + let currentOrderSnap = await orderAfterUpdate.ref.get(); + if (currentOrderSnap.data()?.happyPlace !== happyPlace) { + logger.info(`Order ${event.params.orderId} happy place changed during generation. Aborting.`); + } else { + if(!moderation.isAllowed) { + logger.info(`Order ${event.params.orderId} contains profane language.`); + } + + await latteOrderMetadataRef.update({ + isHappyPlaceApproved: moderation.isAllowed, + happyPlaceModerationReason: moderation.moderationReason, + ...(moderation.isAllowed && { imageBatchId: latteImageBatchDoc.id }) + }); + } + } + }); + +async function createImageBatchDoc(db: Firestore, happyPlace: string, eventOrderId: string, orderAfterUpdateId: string) { + const latteImageBatchesCollection = db.collection("latteImageBatches"); + const generatePromptRequest = await generatePromptsForHappyPlace(happyPlace, eventOrderId); + const latteImageBatchDoc = latteImageBatchesCollection.doc(); + await latteImageBatchDoc.create({ + orderId: orderAfterUpdateId, + createdAt: FieldValue.serverTimestamp(), + expireAt: Timestamp.fromMillis(Date.now() + SEVEN_DAYS_MILLIS), + }); + + return { generatePromptRequest, latteImageBatchDoc }; +} diff --git a/genlatte/firebase/functions/src/ordersEvents/submitOrder.ts b/genlatte/firebase/functions/src/ordersEvents/submitOrder.ts new file mode 100644 index 0000000..96c100d --- /dev/null +++ b/genlatte/firebase/functions/src/ordersEvents/submitOrder.ts @@ -0,0 +1,118 @@ +import { onCall, HttpsError } from "firebase-functions/https"; +import { DocumentReference, FieldValue, getFirestore } from "firebase-admin/firestore"; +import { db } from "../common"; +import { DATABASE_NAME } from "./common"; +import { logger } from "firebase-functions/logger"; +import { getRemoteConfig } from "firebase-admin/remote-config"; + +export const submitOrder = onCall({ + +}, async (request) => { + logger.info(`submitOrder called with data: ${JSON.stringify(request.data)}`); + const { orderId } = request.data; + + logger.info(`submitOrder called with orderId: ${orderId}`); + if (!orderId) { + throw new HttpsError( + "invalid-argument", + "The 'orderId' argument is required." + ); + } + + const latteOrderMetadataRef = db.collection("latteOrderMetadata").doc(orderId); + const latteOrderRef = db.collection("latteOrders").doc(orderId); + const metadataDocSnap = await latteOrderMetadataRef.get(); + const orderDocSnap = await latteOrderRef.get(); + + if (!metadataDocSnap.exists || !orderDocSnap.exists) { + throw new HttpsError("not-found", "Order not found."); + } + + const metadata = metadataDocSnap.data(); + const orderData = orderDocSnap.data(); + if (!metadata || !orderData) { + throw new HttpsError("not-found", `Order ${orderId} has empty data.`); + } + logger.info(`latteOrderMetadata data: ${JSON.stringify(metadata)}`); + + if(metadata.orderNumber === null || metadata.orderNumber === undefined) { + await assignOrderNumber(orderId, latteOrderMetadataRef); + } + + if (metadata.imageUrl === null) { + logger.info(`submitOrder missing imageUrl`); + throw new HttpsError( + "failed-precondition", + "Cannot submit order until an image has been selected." + ); + } + + await latteOrderMetadataRef.update({ + status: "submitted", + orderSubmittedTime: FieldValue.serverTimestamp(), + }); +}); + +const assignOrderNumber = async (orderId: string, latteOrderMetadataRef: DocumentReference) => { + const db = getFirestore(DATABASE_NAME); + const orderRef = db.collection("latteOrders").doc(orderId); + + if (!orderRef) { + logger.error("No order reference found in event data."); + return; + } + + // Format today's date as YYYY-MM-DD + const now = new Date(); + const dateId = now.toISOString().split("T")[0]; + + // Fetch NUM_SHARDS from Remote Config + // Control from remote config to limit the requirement + // to redeploy the function + const rc = getRemoteConfig(); + let NUM_SHARDS = 10; + try { + const template = await rc.getServerTemplate({ + defaultConfig: { + NUM_SHARDS: 10, + }, + }); + const config = template.evaluate(); + NUM_SHARDS = config.getNumber("NUM_SHARDS_BRANCH_SUFFIX"); + logger.info(`Using Remote Config NUM_SHARDS: ${NUM_SHARDS}`); + } catch (err) { + logger.warn( + "Failed to fetch Remote Config, using default NUM_SHARDS=10", err); + } + + const shardId = Math.floor(Math.random() * NUM_SHARDS); + const counterRef = db.collection("orderCount").doc(dateId) + .collection("shards").doc(shardId.toString()); + + try { + await db.runTransaction(async (transaction) => { + const counterDoc = await transaction.get(counterRef); + let currentShardCount = 0; + + if (counterDoc.exists) { + currentShardCount = counterDoc.data()?.count || 0; + } + + const newShardCount = currentShardCount + 1; + + // Calculate a unique order number across shards: + // (newCount - 1) * NUM_SHARDS + shardId + 1 + const orderNumber = (newShardCount - 1) * NUM_SHARDS + shardId + 1; + + transaction.set(counterRef, { count: newShardCount }, { merge: true }); + transaction.update(orderRef, { orderNumber: orderNumber }); + transaction.update(latteOrderMetadataRef, { orderNumber: orderNumber }); + }); + + logger.info( + `Successfully assigned orderNumber to ${orderId} ` + + `for date ${dateId} (Shard ${shardId})`); + } catch (error) { + logger.error(`Error processing order ${orderId}:`, error); + } +} \ No newline at end of file diff --git a/genlatte/firebase/functions/src/reviser/CONTEXT.md b/genlatte/firebase/functions/src/reviser/CONTEXT.md new file mode 100644 index 0000000..2472436 --- /dev/null +++ b/genlatte/firebase/functions/src/reviser/CONTEXT.md @@ -0,0 +1,33 @@ +# Reviser Module (`firebase/functions/src/reviser/`) + +## Purpose +This directory contains the logic for the "Image Revision" system. It uses multimodal LLMs (specifically Gemini 3 Flash) to analyze generated images and create interactive refinement questions for the user. This allows for a "human-in-the-loop" iterative design process. + +## Detailed File Overviews + +- **`reviseImageQuestionGen.ts`**: + - **Description**: The primary service for generating structured follow-up questions from an image. + - **Core Logic**: Orchestrates a multi-step AI workflow: + 1. Analyzes a base64-encoded image and generates a list of potential refinement questions. + 2. Post-processes the questions to enrich them with valid options (for multiple-choice) or min/max labels (for slider-based questions). + 3. Returns a structured JSON array suitable for frontend rendering. + +- **`reviserConfigs.ts`**: + - **Description**: Defines the JSON schema and configuration for GenAI models. + - **Core Logic**: Provides `GenerateContentConfig` objects that use constrained output formats to ensure Gemini returns valid, parseable JSON for questions, slider values, and text options. + +- **`reviserPrompts.ts`**: + - **Description**: Management of prompt templates used by the reviser. + - **Core Logic**: Defines the system instructions that guide the AI to focus on specific image attributes (color, composition, style) when proposing refinements. + +## Dependencies/Relationships + +- **Vertex AI (Gemini 3 Flash)**: Optimized for multimodal reasoning and fast structured output. +- **Common Module**: Uses `withRetry` from `../common` to handle the iterative nature of the AI service calls. +- **Tasks Module**: Invoked by `taskGenerateQuestions` in `../tasks/`. + +## Usage/Exports + +### Core Services +- `reviseImageQuestionGenerator`: The main entry point that takes an image and returned enriched question objects with unique IDs and metadata. +- `generateAnswersOrLabels`: Utility for secondary AI calls to populate question options. diff --git a/genlatte/firebase/functions/src/reviser/reviseImageQuestionGen.ts b/genlatte/firebase/functions/src/reviser/reviseImageQuestionGen.ts new file mode 100644 index 0000000..6f3d532 --- /dev/null +++ b/genlatte/firebase/functions/src/reviser/reviseImageQuestionGen.ts @@ -0,0 +1,121 @@ +import { HttpsError } from "firebase-functions/v2/https"; +import * as logger from "firebase-functions/logger"; +import { GenerateContentConfig, GoogleGenAI } from '@google/genai'; +import { withRetry } from "../common"; +import { fetchRemoteConfigValues } from "../common/fetchRemoteConfig"; +import { questionGenConfig, sliderValueGenConfig, textValueGenConfig } from "./reviserConfigs"; + +const MODEL = "gemini-3-flash-preview"; + +const getChat = (config?: GenerateContentConfig) => { + const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT; + const genAI = new GoogleGenAI({ vertexai: true, project: projectId, location: 'global' }); + const chat = genAI.chats.create({ model: MODEL, config: config }); + return chat; +} + +const generateQuestions = async(base64ImageFile: string, questionGenPromptInstructions: string, orderId?: string) => { + if (!base64ImageFile || typeof base64ImageFile !== 'string') { + return; + } + + try { + const chat = getChat(questionGenConfig); + + const content = [ + { + inlineData: { + mimeType: "image/png", + data: base64ImageFile, + }, + }, + {text: questionGenPromptInstructions} + ] + + return await withRetry(async () => { + logger.info("Generating questions with image "); + const result = await chat.sendMessage({ message: content }); + if (!result.text) { + logger.error("no text returned from Gemini"); + throw new Error("No text returned from Gemini"); + } + + try { + logger.info("response from Gemini: ", result.text); + const questions = JSON.parse(result.text); + return questions; + } catch (e) { + logger.warn("Failed to parse JSON response from Gemini, attempting fallback", e); + throw new Error("Invalid output format from GenAI"); + } + }, 3, 1000, orderId); + + } catch (e: any) { + logger.error("Error in generateQuestions function", e); + throw new HttpsError("internal", "An error occurred while generating revisions.", e.message); + } +} + +export const generateAnswersOrLabels = async(promptWithQuestionEmbedded: string, config: any, orderId?: string) => { + if (!promptWithQuestionEmbedded || typeof promptWithQuestionEmbedded !== 'string') { + return; + } + + try { + const chat = getChat(config); + + const content = [ + {text: promptWithQuestionEmbedded} + ] + + return await withRetry(async () => { + const result = await chat.sendMessage({ message: content }); + if (!result.text) { + logger.error("no text returned from Gemini"); + throw new Error("No text returned from Gemini"); + } + + try { + logger.info("response from Gemini: ", result.text); + const questions = JSON.parse(result.text); + return questions; + } catch (e) { + logger.warn("Failed to parse JSON response from Gemini, attempting fallback", e); + throw new Error("Invalid output format from GenAI"); + } + }, 3, 1000, orderId); + } catch (e: any) { + logger.error("Error in generateAnswersOrLabels function", e); + throw new HttpsError("internal", "An error occurred while generating revisions.", e.message); + } +} + +export const reviseImageQuestionGenerator = async (base64ImageFile: string, orderId?: string) => { + if (!base64ImageFile || typeof base64ImageFile !== 'string') { + return; + } + + const { questionGenPromptInstructions, sliderValueGenPromptInstructions, textValueGenPromptInstructions } = await fetchRemoteConfigValues(); + + const questions = await generateQuestions(base64ImageFile, questionGenPromptInstructions, orderId); + const questionsWithValues = await Promise.all(questions.map(async (question: any) => { + let fullQuestion = question; + if (question.type === "multipleChoiceQuestion") { + logger.info("Generating acceptable answers for question: ", question.question); + const prompt = textValueGenPromptInstructions.replace("{{userSuppliedQuestion}}", question.question); + fullQuestion.acceptableAnswers = await generateAnswersOrLabels(prompt, textValueGenConfig, orderId); + } + if (question.type === "zeroToOneQuestion" || question.type === "negativeOneToOneQuestion") { + logger.info("Generating slider values for question: ", question.question); + const prompt = sliderValueGenPromptInstructions.replace("{{userSuppliedQuestion}}", question.question); + const sliderValues = await generateAnswersOrLabels(prompt, sliderValueGenConfig, orderId); + fullQuestion.minValueLabel = sliderValues.minValueLabel; + fullQuestion.maxValueLabel = sliderValues.maxValueLabel; + } + return fullQuestion; + })); + + logger.info("questionsWithValues: ", questionsWithValues); + console.log("questionsWithValues: ", questionsWithValues); + return questionsWithValues; +} diff --git a/genlatte/firebase/functions/src/reviser/reviserConfigs.ts b/genlatte/firebase/functions/src/reviser/reviserConfigs.ts new file mode 100644 index 0000000..19e0b83 --- /dev/null +++ b/genlatte/firebase/functions/src/reviser/reviserConfigs.ts @@ -0,0 +1,117 @@ +import { HarmBlockThreshold, HarmCategory } from "@google/genai"; + +export const questionGenConfig = { + maxOutputTokens: 65535, + temperature: 0.7, + topP: 0.95, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + } + ], + responseMimeType: 'application/json', + responseSchema: { + type: "ARRAY", + description: "A list of 2 to 4 questions to tweak the supplied image.", + items: { + type: "OBJECT", + properties: { + question: { + type: "STRING", + description: "The question to ask the user to refine their prompt." + }, + type: { + type: "STRING", + description: "The type of answer expected. Must be one of: 'multipleChoiceQuestion', 'zeroToOneQuestion', 'negativeOneToOneQuestion', 'textQuestion'" + }, + }, + required: ["question", "type"] + }, + }, + }; + +export const sliderValueGenConfig = { + maxOutputTokens: 65535, + temperature: 0.7, + topP: 0.95, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + } + ], + responseMimeType: 'application/json', + responseSchema: { + type: "OBJECT", + properties: { + minValueLabel: { + type: "STRING", + description: "The label to display for the minimum value of the slider. Only used for 'zeroToOneQuestion' and 'negativeOneToOneQuestion'" + }, + maxValueLabel: { + type: "STRING", + description: "The label to display for the maximum value of the slider. Only used for 'zeroToOneQuestion' and 'negativeOneToOneQuestion'" + }, + }, + required: ["minValueLabel", "maxValueLabel"] + }, + }; + +export const textValueGenConfig = { + maxOutputTokens: 65535, + temperature: 0.7, + topP: 0.95, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + } + ], + responseMimeType: 'application/json', + responseSchema: { + type: "ARRAY", + description: "A list of 2 to 4 acceptable answers based on the supplied question", + items: { + type: "STRING", + description: "A single acceptable answer based on the supplied question" + }, + }, + }; + \ No newline at end of file diff --git a/genlatte/firebase/functions/src/sendToPrinter.ts b/genlatte/firebase/functions/src/sendToPrinter.ts new file mode 100644 index 0000000..8635e31 --- /dev/null +++ b/genlatte/firebase/functions/src/sendToPrinter.ts @@ -0,0 +1,87 @@ +import { onCall, HttpsError } from "firebase-functions/v2/https"; +import sharp from 'sharp'; +import { db } from "./common"; +import { logger } from "firebase-functions"; +const headers = { + "Redacted": "headers", +}; + +async function sendUploadMessageCode(imageBufferRaw: Buffer, machineId: string, blackAndWhiteMachine: boolean = true) { + try { + const image = await sharp(imageBufferRaw) + .resize(800, 800); + + if (blackAndWhiteMachine) { + image.grayscale(); + } + + const imageBuffer = await image.jpeg().toBuffer(); + + const base64Image = imageBuffer.toString('base64'); + + const payload = `redacted`; + + const response = await fetch("redacted_url", { + method: "POST", + headers: { + ...headers, + "Content-Length": Buffer.byteLength(payload, 'utf8').toString() + }, + body: payload + }); + + const data = await response.text(); + console.log("Response status:", response.status); + console.log("Response data:", data); + } catch (error) { + console.error("Error sending request:", error); + } +} + +/** + * Looks up a machine by name. + * @param machineName The name of the machine to look up. + * @returns The machine ID and whether it is a black and white machine. + */ +const machineLookupByName = async (machineName: string): Promise<{ id: string, isBlackAndWhite: boolean }> => { + const machineRef = db.collection('machines').where('name', '==', machineName); + const snapshot = await machineRef.get(); + if (snapshot.empty) { + throw new HttpsError("not-found", "Machine not found."); + } + return { id: snapshot.docs[0].id, isBlackAndWhite: snapshot.docs[0].data().isBlackAndWhite }; +} + +/** + * Sends an image to the printer. + * @param imagePath The gs:// path to the image to send. + */ +export const sendToPrinter = onCall({ memory: "1GiB" }, async (request) => { + logger.info("Request data:", JSON.stringify(request.data)); + let { imagePath, machineName } = request.data; + + if (!machineName || typeof machineName !== 'string') { + machineName = "Barry White"; + } + + if (!imagePath || typeof imagePath !== 'string' || !imagePath.startsWith('https://')) { + throw new HttpsError("invalid-argument", "The function must be called with a valid 'imagePath' string argument starting with 'https://'."); + } + + const machine = await machineLookupByName(machineName); + logger.info("Machine found:", JSON.stringify(machine)); + + try { + const url = new URL(imagePath); + + const imageBufferRaw = await fetch(url.toString()).then(res => res.arrayBuffer()); + + const machineIdInt = parseInt(machine.id); + const magicNumber = machineIdInt ^ 21937163; + + await sendUploadMessageCode(Buffer.from(imageBufferRaw), magicNumber.toString(), machine.isBlackAndWhite); + return { success: true }; + } catch (e: any) { + throw new HttpsError("internal", "An error occurred while sending the image to the printer.", e.message); + } +}); diff --git a/genlatte/firebase/functions/src/tasks/CONTEXT.md b/genlatte/firebase/functions/src/tasks/CONTEXT.md new file mode 100644 index 0000000..2aea412 --- /dev/null +++ b/genlatte/firebase/functions/src/tasks/CONTEXT.md @@ -0,0 +1,37 @@ +# Tasks Module (`firebase/functions/src/tasks/`) + +## Purpose +This directory contains Firebase Cloud Task handlers used for reliable, long-running background processing. By offloading resource-intensive generative AI operations (image and question generation) to Cloud Tasks, the system ensures higher reliability with built-in retry logic and configurable timeouts. + +## Detailed File Overviews + +- **`generateImage.ts`**: + - **Description**: Handles requested image generation tasks. + - **Core Logic**: + - Retrieves order and batch context from Firestore. + - Calls the image generation engine (`generateImageWithPro`) to produce a base64 image via Vertex AI. + - Persists the resulting image to Firebase Storage and makes it public. + - Generates a text description of the image content. + - Updates the `latteImageBatch` document with the new image URL and metadata. + - Enqueues a subsequent `taskGenerateQuestions` for initial generations. + +- **`generateQuestions.ts`**: + - **Description**: Orchestrates the generation of AI-driven follow-up questions for image refinement. + - **Core Logic**: + - Downloads the generated image from Storage. + - Invokes the `reviseImageQuestionGenerator` to analyze the image and generate context-aware questions. + - Updates the Firestore batch record with the generated questions, assigning unique IDs to each for client-side tracking. + +## Dependencies/Relationships + +- **Vertex AI / Gemini**: Indirectly utilized via internal service wrappers in `../images` and `../reviser`. +- **Firebase Admin SDK**: Used for Firestore updates (`admin/firestore`), Storage management (`admin/storage`), and enqueuing further tasks (`admin/functions`). +- **Cloud Task Queue**: These handlers are triggered by the Cloud Task infrastructure based on queue names defined in the service configuration. + +## Usage/Exports + +### Cloud Task Handlers +- `taskGenerateImage`: Background worker for image creation. +- `taskGenerateQuestions`: Background worker for image analysis and question generation. + +These functions use standard `onTaskDispatched` configurations with a retry limit of 5 attempts and a 540-second timeout to accommodate generative AI latency. diff --git a/genlatte/firebase/functions/src/tasks/generateImage.ts b/genlatte/firebase/functions/src/tasks/generateImage.ts new file mode 100644 index 0000000..3a6ccff --- /dev/null +++ b/genlatte/firebase/functions/src/tasks/generateImage.ts @@ -0,0 +1,60 @@ +import { onTaskDispatched } from "firebase-functions/tasks"; +import { generateDescription, generateImageWithPro } from "../images"; +import { getStorage } from "firebase-admin/storage"; +import { getFirestore } from "firebase-admin/firestore"; +import { getFunctions } from "firebase-admin/functions"; +import { logger } from "firebase-functions/logger"; +import { fetchRemoteConfigValues } from "../common/fetchRemoteConfig"; + +const DATABASE_NAME = process.env.FUNCTIONS_EMULATOR === 'true' ? '(default)' : 'DATABASE_ID_PLACEHOLDER'; +const db = getFirestore(DATABASE_NAME); + +export const taskGenerateImage = onTaskDispatched({ + retryConfig: { + maxAttempts: 5, + minBackoffSeconds: 1, + maxBackoffSeconds: 20, + }, + concurrency: 1, + minInstances: 8, + maxInstances: 80, + memory: "8GiB", + timeoutSeconds: 540, +}, async (request) => { + const { prompt, imageBatchId, index, isRevision = false } = request.data; + const latteImageBatchDoc = db.collection("latteImageBatches").doc(imageBatchId); + const latteImageBatchData = await latteImageBatchDoc.get(); + const orderId = latteImageBatchData.data()?.orderId; + logger.info(`taskGenerateImage called for orderId: ${orderId}`); + + const imagePath = `latteImages/${imageBatchId}/${index}.png`; + // run multiple of these using the remote config value AIR_CONDITIONING_QUOTENT and take the first + // result for the image + const {imageParallelism} = await fetchRemoteConfigValues(); + const generateImagesPromises = []; + for(let i = 0; i < imageParallelism; i++) { + generateImagesPromises.push(generateImageWithPro(prompt, orderId)); + } + const result = await Promise.any(generateImagesPromises); + const storageImage = getStorage().bucket().file(imagePath); + const base64String = result.image.split(',')[1]; + const imageBuffer = Buffer.from(base64String, 'base64'); + await storageImage.save(imageBuffer, { contentType: "image/png" }); + await storageImage.makePublic(); + const imageUrl = await storageImage.publicUrl(); + await latteImageBatchDoc.update({ + [`image${index}`]: { + id: crypto.randomUUID(), + imageUrl: imageUrl, + prompt: result.prompt, + description: await generateDescription(base64String, orderId), + } + }); + if(!isRevision) { + await getFunctions().taskQueue("taskGenerateQuestionsBranchSuffix").enqueue({ + imageBatchId: imageBatchId, + index: index, + storageImagePath: imagePath, + }); + } +}); \ No newline at end of file diff --git a/genlatte/firebase/functions/src/tasks/generateQuestions.ts b/genlatte/firebase/functions/src/tasks/generateQuestions.ts new file mode 100644 index 0000000..f90b0bb --- /dev/null +++ b/genlatte/firebase/functions/src/tasks/generateQuestions.ts @@ -0,0 +1,41 @@ +import { onTaskDispatched } from "firebase-functions/tasks"; +import { getStorage } from "firebase-admin/storage"; +import { getFirestore } from "firebase-admin/firestore"; +import { reviseImageQuestionGenerator } from "../reviser/reviseImageQuestionGen"; +import { logger } from "firebase-functions/logger"; + +const DATABASE_NAME = process.env.FUNCTIONS_EMULATOR === 'true' ? '(default)' : 'DATABASE_ID_PLACEHOLDER'; +const db = getFirestore(DATABASE_NAME); + +export const taskGenerateQuestions = onTaskDispatched({ + retryConfig: { + maxAttempts: 5, + minBackoffSeconds: 1, + maxBackoffSeconds: 20, + }, + memory: "8GiB", + timeoutSeconds: 540, +}, async (request) => { + const { imageBatchId, index, storageImagePath } = request.data; + const latteImageBatchDoc = db.collection("latteImageBatches").doc(imageBatchId); + const latteImageBatchData = await latteImageBatchDoc.get(); + const orderId = latteImageBatchData.data()?.orderId; + logger.info(`taskGenerateQuestions called for orderId: ${orderId}`); + + const storageImage = getStorage().bucket().file(storageImagePath); + const [imageBuffer] = await storageImage.download(); + const base64String = imageBuffer.toString('base64'); + + const questions = await reviseImageQuestionGenerator(base64String, orderId); + console.log(questions); + logger.info(questions); + if (questions) { + const questionsWithIds = questions.map((q: any) => ({ + ...q, + id: crypto.randomUUID() + })); + await latteImageBatchDoc.update({ + [`image${index}.questions`]: questionsWithIds, + }); + } +}); \ No newline at end of file diff --git a/genlatte/firebase/functions/src/userEvents/CONTEXT.md b/genlatte/firebase/functions/src/userEvents/CONTEXT.md new file mode 100644 index 0000000..bd045d2 --- /dev/null +++ b/genlatte/firebase/functions/src/userEvents/CONTEXT.md @@ -0,0 +1,13 @@ +# User Event Functions + +**Purpose:** +Provides administrative logic handling Identity, Authentication, and Role-Based claims allocation. + +**Detailed File Overviews:** + +- `claim.ts`: + - **Description**: User creation interceptor. + - **Core Logic**: Listens at endpoints for new user auth signups. Parses metadata, verifies admin keys or internal configurations, and assigns Custom User Claims (e.g., `moderator: true`, `kiosk: true`) using `admin.auth().setCustomUserClaims()` which authorizes access boundaries later inside the `genlatte-ui` routing layer. + +**Dependencies/Relationships:** +- Determines the data boundary utilized actively by `genlatte-ui/lib/src/core/auth/auth_bloc.dart`. diff --git a/genlatte/firebase/functions/src/userEvents/claim.ts b/genlatte/firebase/functions/src/userEvents/claim.ts new file mode 100644 index 0000000..5f717cd --- /dev/null +++ b/genlatte/firebase/functions/src/userEvents/claim.ts @@ -0,0 +1,63 @@ +import { + onDocumentCreated, + onDocumentDeleted, +} from "firebase-functions/v2/firestore"; +import {getAuth} from "firebase-admin/auth"; +import * as logger from "firebase-functions/logger"; + +/** + * Triggered when a document is created at roles/{role}/users/{uid}. + * Grants the specified custom claim to the user. + */ +export const onUserRoleCreated = onDocumentCreated( + { + document: "roles/{role}/users/{uid}", + database: "DATABASE_ID_PLACEHOLDER", + }, + async (event) => { + const {role, uid} = event.params; + + try { + const auth = getAuth(); + const user = await auth.getUser(uid); + const existingClaims = user.customClaims || {}; + + const newClaims = {...existingClaims, [role]: true}; + await auth.setCustomUserClaims(uid, newClaims); + + logger.info(`Granted role '${role}' to user ${uid}`); + } catch (error) { + logger.error(`Error granting role '${role}' to user ${uid}:`, error); + } + }); + +/** + * Triggered when a document is deleted at roles/{role}/users/{uid}. + * Removes the specified custom claim from the user. + */ +export const onUserRoleDeleted = onDocumentDeleted( + { + document: "roles/{role}/users/{uid}", + database: "DATABASE_ID_PLACEHOLDER", + }, + async (event) => { + const {role, uid} = event.params; + + try { + const auth = getAuth(); + const user = await auth.getUser(uid); + const existingClaims = user.customClaims || {}; + + if (role in existingClaims) { + const newClaims = {...existingClaims}; + delete newClaims[role]; + await auth.setCustomUserClaims(uid, newClaims); + logger.info(`Removed role '${role}' from user ${uid}`); + } else { + logger.info( + `User ${uid} did not have role '${role}'. No action taken.`); + } + } catch (error) { + logger.error(`Error removing role '${role}' from user ${uid}:`, error); + } + }); diff --git a/genlatte/firebase/functions/src/userEvents/index.ts b/genlatte/firebase/functions/src/userEvents/index.ts new file mode 100644 index 0000000..6bdcdf1 --- /dev/null +++ b/genlatte/firebase/functions/src/userEvents/index.ts @@ -0,0 +1,2 @@ +import { onUserRoleCreated, onUserRoleDeleted } from "./claim"; +export { onUserRoleCreated, onUserRoleDeleted }; diff --git a/genlatte/firebase/functions/tsconfig.dev.json b/genlatte/firebase/functions/tsconfig.dev.json new file mode 100644 index 0000000..3d85e61 --- /dev/null +++ b/genlatte/firebase/functions/tsconfig.dev.json @@ -0,0 +1,5 @@ +{ + "include": [ + // ".eslintrc.js" + ] +} \ No newline at end of file diff --git a/genlatte/firebase/functions/tsconfig.json b/genlatte/firebase/functions/tsconfig.json new file mode 100644 index 0000000..b85fa69 --- /dev/null +++ b/genlatte/firebase/functions/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "esModuleInterop": true, + "moduleResolution": "nodenext", + "noImplicitReturns": true, + "noUnusedLocals": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "skipLibCheck": true, + "target": "es2021" + }, + "compileOnSave": true, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/genlatte/firebase/inject_prompts.js b/genlatte/firebase/inject_prompts.js new file mode 100644 index 0000000..895a290 --- /dev/null +++ b/genlatte/firebase/inject_prompts.js @@ -0,0 +1,60 @@ +const fs = require('fs'); +const path = require('path'); + +const PROMPTS_DIR = path.join(__dirname, 'functions/src/common/prompts'); +const REMOTE_CONFIG_PATH = path.join(__dirname, 'remoteconfig.json'); + +function extractStringFromTs(content) { + const firstTick = content.indexOf('`'); + const lastTick = content.lastIndexOf('`'); + if (firstTick === -1 || lastTick === -1 || firstTick === lastTick) { + return content; + } + return content.substring(firstTick + 1, lastTick); +} + +function main() { + if (!fs.existsSync(REMOTE_CONFIG_PATH)) { + console.error(`Error: ${REMOTE_CONFIG_PATH} not found.`); + process.exit(1); + } + + const configRaw = fs.readFileSync(REMOTE_CONFIG_PATH, 'utf8'); + let config; + try { + config = JSON.parse(configRaw); + } catch (e) { + console.error(`Error parsing JSON in ${REMOTE_CONFIG_PATH}: ${e}`); + process.exit(1); + } + + const promptMappings = { + 'MODERATION_PROMPT_BRANCH_SUFFIX': 'moderation.ts', + 'POWERUP_PROMPT_BRANCH_SUFFIX': 'powerup.ts', + 'DESCRIPTION_PROMPT_BRANCH_SUFFIX': 'description.ts', + 'QUESTION_GEN_PROMPT_BRANCH_SUFFIX': 'reviserQuestion.ts', + 'SLIDER_VALUE_GEN_PROMPT_BRANCH_SUFFIX': 'reviserSlider.ts', + 'TEXT_VALUE_GEN_PROMPT_BRANCH_SUFFIX': 'reviserText.ts' + }; + + for (const [configKey, promptFilename] of Object.entries(promptMappings)) { + const promptPath = path.join(PROMPTS_DIR, promptFilename); + if (fs.existsSync(promptPath)) { + const rawContent = fs.readFileSync(promptPath, 'utf8'); + const promptContent = promptFilename.endsWith('.ts') ? extractStringFromTs(rawContent) : rawContent; + + if (config.parameters && config.parameters[configKey]) { + config.parameters[configKey].defaultValue.value = promptContent; + console.log(`Injected ${promptFilename} into ${configKey}`); + } else { + console.warn(`Warning: ${configKey} not found in remote config parameters.`); + } + } else { + console.warn(`Warning: ${promptPath} not found.`); + } + } + + fs.writeFileSync(REMOTE_CONFIG_PATH, JSON.stringify(config, null, 4)); +} + +main(); diff --git a/genlatte/firebase/merge_remote_config.js b/genlatte/firebase/merge_remote_config.js new file mode 100644 index 0000000..1110386 --- /dev/null +++ b/genlatte/firebase/merge_remote_config.js @@ -0,0 +1,38 @@ +const fs = require('fs'); + +/** + * Merges a local Remote Config template into a server-side template. + * Overwrites server-side parameters with local ones, but preserves + * server-side parameters that are not in the local file. + */ +function mergeTemplates(serverPath, localPath) { + try { + const serverTemplate = JSON.parse(fs.readFileSync(serverPath, 'utf8')); + const localTemplate = JSON.parse(fs.readFileSync(localPath, 'utf8')); + + // Ensure parameters objects exist + serverTemplate.parameters = serverTemplate.parameters || {}; + const localParameters = localTemplate.parameters || {}; + + // Merge parameters + Object.keys(localParameters).forEach(key => { + console.log(`Overriding/Adding parameter: ${key}`); + serverTemplate.parameters[key] = localParameters[key]; + }); + + // Write back to serverPath + fs.writeFileSync(serverPath, JSON.stringify(serverTemplate, null, 2)); + console.log('Successfully merged templates.'); + } catch (error) { + console.error('Error merging templates:', error.message); + process.exit(1); + } +} + +const args = process.argv.slice(2); +if (args.length < 2) { + console.error('Usage: node merge_remote_config.js '); + process.exit(1); +} + +mergeTemplates(args[0], args[1]); diff --git a/genlatte/firebase/package.json b/genlatte/firebase/package.json new file mode 100644 index 0000000..ccc7f65 --- /dev/null +++ b/genlatte/firebase/package.json @@ -0,0 +1,20 @@ +{ + "name": "firebase", + "version": "1.0.0", + "description": "", + "main": "create_users.js", + "scripts": { + "test": "firebase emulators:exec --project demo-latteart-rules --only firestore 'mocha tests/**/*.test.js'" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "devDependencies": { + "@firebase/rules-unit-testing": "^5.0.0", + "firebase": "^12.10.0", + "firebase-admin": "^13.7.0", + "firebase-tools": "^15.10.1", + "mocha": "^11.7.5" + } +} diff --git a/genlatte/firebase/remoteconfig.json b/genlatte/firebase/remoteconfig.json new file mode 100644 index 0000000..3ef02ad --- /dev/null +++ b/genlatte/firebase/remoteconfig.json @@ -0,0 +1,74 @@ +{ + "parameters": { + "IMAGE_PARALLELISM": { + "defaultValue": { + "value": "4" + }, + "valueType": "NUMBER", + "description": "How many images we should be generating at once." + }, + "NUM_SHARDS_BRANCH_SUFFIX": { + "defaultValue": { + "value": "1" + }, + "valueType": "NUMBER", + "description": "Number of shards for the order counter to avoid contention." + }, + "MODERATION_PROMPT_BRANCH_SUFFIX": { + "defaultValue": { + "value": "You are an expert content moderation bot.\n\n**TASK**\nYour goal is to determine if a given string of text is inappropriate to be used as a username or \"Happy Place\".\n\n**CRITERIA FOR INAPPROPRIATE CONTENT**\nA string is considered inappropriate if it contains:\n1. **Profanity:** Obscene or vulgar language.\n2. **Hate Speech:** Attacks on individuals or groups based on identity.\n3. **Glorification of Violence:** Promoting or celebrating violent acts.\n4. **Trivialization of Tragedy:** Using historical atrocities or disasters in a trivial context (e.g., \"1945 Hiroshima\" as a happy place).\n5. **Contemporary issues:** Referencing current news cycle items, political figures, polarizing stories, etc\n\n**INSTRUCTIONS**\n- Analyze the input text.\n- If the text meets any of the criteria above, it is NOT allowed. Set `isAllowed` to `false` and provide a `moderationReason`.\n- If the text is acceptable, it IS allowed. Set `isAllowed` to `true` and provide an empty `moderationReason`.\n- Your output must be valid JSON matching the specified schema.\n\n**EXAMPLES**\n\nTEXT TO MODERATE: ASSMAN\n{\"isAccepted\": false, \"reason\": \"contains profane language\"}\n\nTEXT TO MODERATE: 1945 Hiroshima\n{\"isAccepted\": false, \"reason\": \"references historical tragedy\"}\n\nTEXT TO MODERATE: ground zero\n{\"isAccepted\": false, \"reason\": \"references historical tragedy\"}\n\nTEXT TO MODERATE: The beach at sunset\n{\"isAccepted\": true, \"reason\": null}\n\nTEXT TO MODERATE: White Power\n{\"isAccepted\": false, \"reason\": \"contains hate speech\"}\n\nTEXT TO MODERATE: Ted Bundy Fan\n{\"isAccepted\": false, \"reason\": \"references a historical crime\"}\n\nTEXT TO MODERATE: A pool party\n{\"isAccepted\": true, \"reason\": null}\n\nTEXT TO MODERATE: A little kids' pool party\n{\"isAccepted\": false, \"reason\": \"references kids\"}\n\nTEXT TO MODERATE: A high school football game\n{\"isAccepted\": false, \"reason\": \"references high school\"}\n\nTEXT TO MODERATE: A middle school choir concert\n{\"isAccepted\": false, \"reason\": \"references middle school\"}\n\nTEXT TO MODERATE: An elementary school play\n{\"isAccepted\": false, \"reason\": \"references elementary school\"}\n\nTEXT TO MODERATE: A Michigan football game\n{\"isAccepted\": true, \"reason\": null}\n\nTEXT TO MODERATE: A Real Madrid match\n{\"isAccepted\": true, \"reason\": null}\n\nTEXT TO MODEATE: ice\n{\"isAccepted\": false, \"reason\": \"contemporary hot topic\"}\n\nTEXT TO MODERATE: Vietnam\n{\"isAccepted\": true, \"reason\": null}\n\nTEXT TO MODERATE: Vietnam War\n{\"isAccepted\": false, \"reason\": \"references historical tragedy\"}\n\nTEXT TO MODERATE: Japan\n{\"isAccepted\": true, \"reason\": null}\n\nTEXT TO MODERATE: Imperial Japan\n{\"isAccepted\": false, \"reason\": \"references historical tragedy\"}\n\nTEXT TO MODERATE: Germany\n{\"isAccepted\": true, \"reason\": null}\n\nTEXT TO MODERATE: Nazi Germany\n{\"isAccepted\": false, \"reason\": \"references historical tragedy\"}\n\nTEXT TO MODERATE: Korea\n{\"isAccepted\": true, \"reason\": null}\n\nTEXT TO MODERATE: South Korea\n{\"isAccepted\": true, \"reason\": null}\n\nTEXT TO MODERATE: Korean War\n{\"isAccepted\": false, \"reason\": \"references historical tragedy\"}\n\nTEXT TO MODERATE: North Korea\n{\"isAccepted\": false, \"reason\": \"references contemporary geopolitical conflict\"}\n\nTEXT TO MODERATE: Iran\n{\"isAccepted\": false, \"reason\": \"references ongoing geopolitical conflict\"}\n\nTEXT TO MODERATE: Kyiv\n{\"isAccepted\": true, \"reason\": null}\n---\n\nTEXT TO MODERATE:" + }, + "valueType": "STRING", + "description": "The prompt used to moderate free form text input for explicit content" + }, + "DESCRIPTION_PROMPT_BRANCH_SUFFIX": { + "defaultValue": { + "value": "" + }, + "valueType": "STRING", + "description": "Prompt for describing an image." + }, + "QUESTION_GEN_PROMPT_BRANCH_SUFFIX": { + "defaultValue": { + "value": "" + }, + "valueType": "STRING", + "description": "Prompt for revising image questions." + }, + "SLIDER_VALUE_GEN_PROMPT_BRANCH_SUFFIX": { + "defaultValue": { + "value": "" + }, + "valueType": "STRING", + "description": "Prompt for generating slider min/max values." + }, + "TEXT_VALUE_GEN_PROMPT_BRANCH_SUFFIX": { + "defaultValue": { + "value": "" + }, + "valueType": "STRING", + "description": "Prompt for generating text-based multiple choice values." + }, + "POWERUP_PROMPT_BRANCH_SUFFIX": { + "defaultValue": { + "value": "Give me a prompt for a photo generation model that is extremely detailed and at least two paragraphs long. Refrain from showing any humans in the output images. Make it a flat vector illustration with a whimsical, minimalist aesthetic.\nKey characteristics include:\nGeometric Simplification: The landscape elements—such as the pine trees and bushes—are reduced to clean, angular, and stylized geometric shapes rather than realistic textures.\nPalette and Gradation: It utilizes a harmonious, soft-toned color palette. While the shapes are flat, the atmosphere is created through subtle linear gradients in the sky and soft, layered color blocking to suggest depth and light.\nContoured Line Work: The composition is defined by clean, consistent-weight outlines that give the characters and objects a structured, pop-up book quality.\nStylized Proportion: The subject exhibits \"chibi-adjacent\" or simplified character design, favoring clear, readable features and an expressive, cheerful personality over anatomical realism.\nGraphic Balance: There is a strong emphasis on intentional negative space and clean, rhythmic curves (such as the S-curve of the path), creating a balanced, orderly, and serene visual flow.\nPhoto image:\n" + }, + "valueType": "STRING", + "description": "Prompt for image generation model." + }, + "IMAGE_MODEL_BRANCH_SUFFIX": { + "defaultValue": { + "value": "gemini-3.1-flash-image-preview" + }, + "valueType": "STRING", + "description": "Model used for prompt enhancement (e.g. text model)." + }, + "TEXT_MODEL_BRANCH_SUFFIX": { + "defaultValue": { + "value": "gemini-3-flash-preview" + }, + "valueType": "STRING", + "description": "Text model used for prompt powerup generation." + } + } +} \ No newline at end of file diff --git a/genlatte/firebase/storage.rules b/genlatte/firebase/storage.rules new file mode 100644 index 0000000..9f33d22 --- /dev/null +++ b/genlatte/firebase/storage.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if false; + } + } +} diff --git a/genlatte/firebase/tests/CONTEXT.md b/genlatte/firebase/tests/CONTEXT.md new file mode 100644 index 0000000..65bfea0 --- /dev/null +++ b/genlatte/firebase/tests/CONTEXT.md @@ -0,0 +1,12 @@ +# Firebase Security Rule Tests + +**Purpose:** +Provides a suite of automated unit tests to verify the integrity and effectiveness of the project's Firestore and Realtime Database security rules. + +**Detailed File Overviews:** +- `defaultDeny.test.js`: Verifies that the "deny by default" security posture is correctly enforced for unauthenticated requests. +- `latteOrders.test.js` / `latteImageBatches.test.js` / etc.: Specific test suites for each collection, ensuring that role-based access control (RBAC) and data validation rules work as expected for CRUD operations. + +**Dependencies/Relationships:** +- Uses the `@firebase/rules-unit-testing` framework. +- Validates the rules defined in the `firebase/firestore.rules` and `firebase/database.rules.json` files. diff --git a/genlatte/firebase/tests/defaultDeny.test.js b/genlatte/firebase/tests/defaultDeny.test.js new file mode 100644 index 0000000..2088353 --- /dev/null +++ b/genlatte/firebase/tests/defaultDeny.test.js @@ -0,0 +1,85 @@ +const { assertFails, assertSucceeds, initializeTestEnvironment } = require('@firebase/rules-unit-testing'); +const { doc, setDoc, updateDoc, getDoc, deleteDoc } = require('firebase/firestore'); +const { readFileSync } = require('fs'); +const path = require('path'); + +let testEnv; + +before(async () => { + testEnv = await initializeTestEnvironment({ + projectId: 'demo-defaultdeny-rules', + firestore: { + rules: readFileSync(path.resolve(__dirname, '../firestore.rules'), 'utf8'), + host: '127.0.0.1', + port: 8080 + }, + }); +}); + +beforeEach(async () => { + await testEnv.clearFirestore(); + + // setup prerequisite data using bypass + await testEnv.withSecurityRulesDisabled(async (context) => { + const db = context.firestore(); + await setDoc(doc(db, 'unknownCollection/existing_doc'), { data: 'secret' }); + }); +}); + +after(async () => { + if (testEnv) { + await testEnv.cleanup(); + } +}); + +describe('Default Deny Rules', () => { + + let baristaDb; + let unauthDb; + let kioskDb; + + beforeEach(() => { + baristaDb = testEnv.authenticatedContext('barista_user', { barista: true }).firestore(); + unauthDb = testEnv.unauthenticatedContext().firestore(); + kioskDb = testEnv.authenticatedContext('kiosk_user', { kiosk: true }).firestore(); + }); + + describe('Read', () => { + it('should deny barista from reading unknown collections', async () => { + const ref = doc(baristaDb, 'unknownCollection/existing_doc'); + await assertFails(getDoc(ref)); + }); + + it('should deny kiosk from reading unknown collections', async () => { + const ref = doc(kioskDb, 'unknownCollection/existing_doc'); + await assertFails(getDoc(ref)); + }); + + it('should deny unauthed from reading unknown collections', async () => { + const ref = doc(unauthDb, 'unknownCollection/existing_doc'); + await assertFails(getDoc(ref)); + }); + }); + + describe('Write', () => { + it('should deny barista from writing to unknown collections', async () => { + const ref = doc(baristaDb, 'unknownCollection/new_doc'); + await assertFails(setDoc(ref, { data: 'new' })); + }); + + it('should deny kiosk from writing to unknown collections', async () => { + const ref = doc(kioskDb, 'unknownCollection/new_doc'); + await assertFails(setDoc(ref, { data: 'new' })); + }); + + it('should deny unauthed from writing to unknown collections', async () => { + const ref = doc(unauthDb, 'unknownCollection/new_doc'); + await assertFails(setDoc(ref, { data: 'new' })); + }); + + it('should deny barista from deleting unknown collections', async () => { + const ref = doc(baristaDb, 'unknownCollection/existing_doc'); + await assertFails(deleteDoc(ref)); + }); + }); +}); diff --git a/genlatte/firebase/tests/latteImageBatches.test.js b/genlatte/firebase/tests/latteImageBatches.test.js new file mode 100644 index 0000000..1bcfe30 --- /dev/null +++ b/genlatte/firebase/tests/latteImageBatches.test.js @@ -0,0 +1,100 @@ +const { assertFails, assertSucceeds, initializeTestEnvironment } = require('@firebase/rules-unit-testing'); +const { doc, setDoc, updateDoc, getDoc, deleteDoc } = require('firebase/firestore'); +const { readFileSync } = require('fs'); +const path = require('path'); + +let testEnv; + +before(async () => { + testEnv = await initializeTestEnvironment({ + projectId: 'demo-latteimagebatches-rules', + firestore: { + rules: readFileSync(path.resolve(__dirname, '../firestore.rules'), 'utf8'), + host: '127.0.0.1', + port: 8080 + }, + }); +}); + +beforeEach(async () => { + await testEnv.clearFirestore(); + + // setup prerequisite data using bypass + await testEnv.withSecurityRulesDisabled(async (context) => { + const db = context.firestore(); + await setDoc(doc(db, 'latteImageBatches/existing_batch'), { status: 'pending' }); + }); +}); + +after(async () => { + if (testEnv) { + await testEnv.cleanup(); + } +}); + +describe('latteImageBatches Rules', () => { + + let kioskDb; + let baristaDb; + let unauthDb; + + beforeEach(() => { + kioskDb = testEnv.authenticatedContext('kiosk_user', { kiosk: true }).firestore(); + baristaDb = testEnv.authenticatedContext('barista_user', { barista: true }).firestore(); + unauthDb = testEnv.unauthenticatedContext().firestore(); + }); + + describe('Read', () => { + it('should allow kiosk to read batches', async () => { + const ref = doc(kioskDb, 'latteImageBatches/existing_batch'); + await assertSucceeds(getDoc(ref)); + }); + + it('should allow barista to read batches', async () => { + const ref = doc(baristaDb, 'latteImageBatches/existing_batch'); + await assertSucceeds(getDoc(ref)); + }); + + it('should deny unauthed from reading batches', async () => { + const ref = doc(unauthDb, 'latteImageBatches/existing_batch'); + await assertFails(getDoc(ref)); + }); + }); + + describe('Write', () => { + it('should allow barista to create batches', async () => { + const ref = doc(baristaDb, 'latteImageBatches/new_batch'); + await assertSucceeds(setDoc(ref, { status: 'new' })); + }); + + it('should deny kiosk from creating batches', async () => { + const ref = doc(kioskDb, 'latteImageBatches/new_batch'); + await assertFails(setDoc(ref, { status: 'new' })); + }); + + it('should deny unauth from creating batches', async () => { + const ref = doc(unauthDb, 'latteImageBatches/new_batch'); + await assertFails(setDoc(ref, { status: 'new' })); + }); + + it('should allow barista to update batches', async () => { + const ref = doc(baristaDb, 'latteImageBatches/existing_batch'); + await assertSucceeds(updateDoc(ref, { status: 'completed' })); + }); + + it('should deny kiosk from updating batches', async () => { + const ref = doc(kioskDb, 'latteImageBatches/existing_batch'); + await assertFails(updateDoc(ref, { status: 'completed' })); + }); + + it('should allow barista to delete batches', async () => { + const ref = doc(baristaDb, 'latteImageBatches/existing_batch'); + await assertSucceeds(deleteDoc(ref)); + }); + + it('should deny kiosk from deleting batches', async () => { + const ref = doc(kioskDb, 'latteImageBatches/existing_batch'); + await assertFails(deleteDoc(ref)); + }); + }); +}); diff --git a/genlatte/firebase/tests/latteOptions.test.js b/genlatte/firebase/tests/latteOptions.test.js new file mode 100644 index 0000000..80289f8 --- /dev/null +++ b/genlatte/firebase/tests/latteOptions.test.js @@ -0,0 +1,83 @@ +const { assertFails, assertSucceeds, initializeTestEnvironment } = require('@firebase/rules-unit-testing'); +const { doc, setDoc, updateDoc, getDoc, deleteDoc } = require('firebase/firestore'); +const { readFileSync } = require('fs'); +const path = require('path'); + +let testEnv; + +before(async () => { + testEnv = await initializeTestEnvironment({ + projectId: 'demo-latteoptions-rules', + firestore: { + rules: readFileSync(path.resolve(__dirname, '../firestore.rules'), 'utf8'), + host: '127.0.0.1', + port: 8080 + }, + }); +}); + +beforeEach(async () => { + await testEnv.clearFirestore(); + + // setup prerequisite data using bypass + await testEnv.withSecurityRulesDisabled(async (context) => { + const db = context.firestore(); + await setDoc(doc(db, 'latteOptions/existing_option'), { values: ['Test'] }); + }); +}); + +after(async () => { + if (testEnv) { + await testEnv.cleanup(); + } +}); + +describe('latteOptions Rules', () => { + + let unauthDb; + let authDb; + + beforeEach(() => { + unauthDb = testEnv.unauthenticatedContext().firestore(); + authDb = testEnv.authenticatedContext('system_user', {}).firestore(); + }); + + describe('Read', () => { + it('should allow unauth users to read options', async () => { + const ref = doc(unauthDb, 'latteOptions/existing_option'); + await assertSucceeds(getDoc(ref)); + }); + + it('should allow auth users to read options', async () => { + const ref = doc(authDb, 'latteOptions/existing_option'); + await assertSucceeds(getDoc(ref)); + }); + }); + + describe('Write', () => { + it('should deny unauth users from creating options', async () => { + const ref = doc(unauthDb, 'latteOptions/new_option'); + await assertFails(setDoc(ref, { values: ['Test2'] })); + }); + + it('should deny auth users from creating options', async () => { + const ref = doc(authDb, 'latteOptions/new_option'); + await assertFails(setDoc(ref, { values: ['Test2'] })); + }); + + it('should deny unauth users from updating options', async () => { + const ref = doc(unauthDb, 'latteOptions/existing_option'); + await assertFails(updateDoc(ref, { values: ['Updated'] })); + }); + + it('should deny unauth users from deleting options', async () => { + const ref = doc(unauthDb, 'latteOptions/existing_option'); + await assertFails(deleteDoc(ref)); + }); + + it('should deny auth users from deleting options', async () => { + const ref = doc(authDb, 'latteOptions/existing_option'); + await assertFails(deleteDoc(ref)); + }); + }); +}); diff --git a/genlatte/firebase/tests/latteOrderMetadata.test.js b/genlatte/firebase/tests/latteOrderMetadata.test.js new file mode 100644 index 0000000..95dd73c --- /dev/null +++ b/genlatte/firebase/tests/latteOrderMetadata.test.js @@ -0,0 +1,100 @@ +const { assertFails, assertSucceeds, initializeTestEnvironment } = require('@firebase/rules-unit-testing'); +const { doc, setDoc, updateDoc, getDoc, deleteDoc } = require('firebase/firestore'); +const { readFileSync } = require('fs'); +const path = require('path'); + +let testEnv; + +before(async () => { + testEnv = await initializeTestEnvironment({ + projectId: 'demo-latteordermetadata-rules', + firestore: { + rules: readFileSync(path.resolve(__dirname, '../firestore.rules'), 'utf8'), + host: '127.0.0.1', + port: 8080 + }, + }); +}); + +beforeEach(async () => { + await testEnv.clearFirestore(); + + // setup prerequisite data using bypass + await testEnv.withSecurityRulesDisabled(async (context) => { + const db = context.firestore(); + await setDoc(doc(db, 'latteOrderMetadata/existing_metadata'), { notes: 'urgent' }); + }); +}); + +after(async () => { + if (testEnv) { + await testEnv.cleanup(); + } +}); + +describe('latteOrderMetadata Rules', () => { + + let kioskDb; + let baristaDb; + let unauthDb; + + beforeEach(() => { + kioskDb = testEnv.authenticatedContext('kiosk_user', { kiosk: true }).firestore(); + baristaDb = testEnv.authenticatedContext('barista_user', { barista: true }).firestore(); + unauthDb = testEnv.unauthenticatedContext().firestore(); + }); + + describe('Read', () => { + it('should allow kiosk to read metadata', async () => { + const ref = doc(kioskDb, 'latteOrderMetadata/existing_metadata'); + await assertSucceeds(getDoc(ref)); + }); + + it('should allow barista to read metadata', async () => { + const ref = doc(baristaDb, 'latteOrderMetadata/existing_metadata'); + await assertSucceeds(getDoc(ref)); + }); + + it('should deny unauthed from reading metadata', async () => { + const ref = doc(unauthDb, 'latteOrderMetadata/existing_metadata'); + await assertFails(getDoc(ref)); + }); + }); + + describe('Write', () => { + it('should allow barista to create metadata', async () => { + const ref = doc(baristaDb, 'latteOrderMetadata/new_metadata'); + await assertSucceeds(setDoc(ref, { notes: 'new' })); + }); + + it('should deny kiosk from creating metadata', async () => { + const ref = doc(kioskDb, 'latteOrderMetadata/new_metadata'); + await assertFails(setDoc(ref, { notes: 'new' })); + }); + + it('should deny unauth from creating metadata', async () => { + const ref = doc(unauthDb, 'latteOrderMetadata/new_metadata'); + await assertFails(setDoc(ref, { notes: 'new' })); + }); + + it('should allow barista to update metadata', async () => { + const ref = doc(baristaDb, 'latteOrderMetadata/existing_metadata'); + await assertSucceeds(updateDoc(ref, { notes: 'updated' })); + }); + + it('should deny kiosk from updating metadata', async () => { + const ref = doc(kioskDb, 'latteOrderMetadata/existing_metadata'); + await assertFails(updateDoc(ref, { notes: 'updated' })); + }); + + it('should allow barista to delete metadata', async () => { + const ref = doc(baristaDb, 'latteOrderMetadata/existing_metadata'); + await assertSucceeds(deleteDoc(ref)); + }); + + it('should deny kiosk from deleting metadata', async () => { + const ref = doc(kioskDb, 'latteOrderMetadata/existing_metadata'); + await assertFails(deleteDoc(ref)); + }); + }); +}); diff --git a/genlatte/firebase/tests/latteOrders.test.js b/genlatte/firebase/tests/latteOrders.test.js new file mode 100644 index 0000000..f16112b --- /dev/null +++ b/genlatte/firebase/tests/latteOrders.test.js @@ -0,0 +1,138 @@ +const { assertFails, assertSucceeds, initializeTestEnvironment } = require('@firebase/rules-unit-testing'); +const { doc, setDoc, updateDoc, getDoc, deleteDoc } = require('firebase/firestore'); +const { readFileSync } = require('fs'); +const path = require('path'); + +let testEnv; + +before(async () => { + testEnv = await initializeTestEnvironment({ + projectId: 'demo-latteart-rules', + firestore: { + rules: readFileSync(path.resolve(__dirname, '../firestore.rules'), 'utf8'), + host: '127.0.0.1', + port: 8080 + }, + }); +}); + +beforeEach(async () => { + await testEnv.clearFirestore(); + + // setup prerequisite data using bypass + await testEnv.withSecurityRulesDisabled(async (context) => { + const db = context.firestore(); + await setDoc(doc(db, 'latteOptions/milks'), { values: ['Whole', 'Skim', 'Oat', 'Almond'] }); + await setDoc(doc(db, 'latteOptions/sweeteners'), { values: ['None', 'Sugar', 'Stevia', 'Caramel'] }); + + // pre-seed an order for testing updates + await setDoc(doc(db, 'latteOrders/existing_order'), { + name: 'Craig' + }); + }); +}); + +after(async () => { + if (testEnv) { + await testEnv.cleanup(); + } +}); + +describe('latteOrders Rules', () => { + + let kioskDb; + let baristaDb; + let unauthDb; + + beforeEach(() => { + kioskDb = testEnv.authenticatedContext('kiosk_user', { kiosk: true }).firestore(); + baristaDb = testEnv.authenticatedContext('barista_user', { barista: true }).firestore(); + unauthDb = testEnv.unauthenticatedContext().firestore(); + }); + + describe('Create', () => { + it('should allow kiosk to create with just a name', async () => { + const ref = doc(kioskDb, 'latteOrders/order1'); + await assertSucceeds(setDoc(ref, { name: 'Latte' })); + }); + + it('should fail if kiosk tries to create with an extra arbitrary field', async () => { + const ref = doc(kioskDb, 'latteOrders/order2'); + await assertFails(setDoc(ref, { name: 'Latte', badField: true })); + }); + + it('should deny barista from creating orders', async () => { + const ref = doc(baristaDb, 'latteOrders/order3'); + await assertFails(setDoc(ref, { name: 'Latte' })); + }); + + it('should deny unauthed from creating orders', async () => { + const ref = doc(unauthDb, 'latteOrders/order4'); + await assertFails(setDoc(ref, { name: 'Latte' })); + }); + }); + + describe('Update', () => { + it('should allow barista to update orders without restriction on milk validity', async () => { + const ref = doc(baristaDb, 'latteOrders/existing_order'); + await assertSucceeds(updateDoc(ref, { milk: 'unlisted-super-milk' })); + }); + + it('should allow kiosk to update order with valid milk', async () => { + const ref = doc(kioskDb, 'latteOrders/existing_order'); + await assertSucceeds(updateDoc(ref, { milk: 'Oat' })); + }); + + it('should fail if kiosk tries to update order with invalid milk', async () => { + const ref = doc(kioskDb, 'latteOrders/existing_order'); + await assertFails(updateDoc(ref, { milk: 'invalid_milk' })); + }); + + it('should allow kiosk to update order with valid sweetener', async () => { + const ref = doc(kioskDb, 'latteOrders/existing_order'); + await assertSucceeds(updateDoc(ref, { sweetener: 'Caramel' })); + }); + + it('should fail if kiosk tries to update order with invalid sweetener', async () => { + const ref = doc(kioskDb, 'latteOrders/existing_order'); + await assertFails(updateDoc(ref, { sweetener: 'salt' })); + }); + + it('should allow kiosk to update image URL', async () => { + const ref = doc(kioskDb, 'latteOrders/existing_order'); + await assertSucceeds(updateDoc(ref, { imageUrl: 'https://example.com/image.png' })); + }); + + it('should allow kiosks to update milk and sweetener at the same time', async () => { + const ref = doc(kioskDb, 'latteOrders/existing_order'); + await assertSucceeds(updateDoc(ref, { milk: 'Oat', sweetener: 'Caramel' })); + }); + }); + + describe('Read & Delete', () => { + it('should allow barista to read orders', async () => { + const ref = doc(baristaDb, 'latteOrders/existing_order'); + await assertSucceeds(getDoc(ref)); + }); + + it('should allow kiosk to read orders', async () => { + const ref = doc(kioskDb, 'latteOrders/existing_order'); + await assertSucceeds(getDoc(ref)); + }); + + it('should deny unauthed from reading orders', async () => { + const ref = doc(unauthDb, 'latteOrders/existing_order'); + await assertFails(getDoc(ref)); + }); + + it('should deny kiosk from deleting orders', async () => { + const ref = doc(kioskDb, 'latteOrders/existing_order'); + await assertFails(deleteDoc(ref)); + }); + + it('should allow barista to delete orders', async () => { + const ref = doc(baristaDb, 'latteOrders/existing_order'); + await assertSucceeds(deleteDoc(ref)); + }); + }); +}); diff --git a/genlatte/genlatte-data/.gitignore b/genlatte/genlatte-data/.gitignore new file mode 100644 index 0000000..000afeb --- /dev/null +++ b/genlatte/genlatte-data/.gitignore @@ -0,0 +1,8 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ +build/** + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/genlatte/genlatte-data/CHANGELOG.md b/genlatte/genlatte-data/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/genlatte/genlatte-data/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/genlatte/genlatte-data/CONTEXT.md b/genlatte/genlatte-data/CONTEXT.md new file mode 100644 index 0000000..81caf45 --- /dev/null +++ b/genlatte/genlatte-data/CONTEXT.md @@ -0,0 +1,22 @@ +# Shared Models Package (genlatte-data) + +## Purpose +A pure-Dart package that defines the core data structures and domain models shared across the entire GenLatte ecosystem. This ensures type safety and consistent data serialization between the Flutter UI, Cloud Functions, and Cloud Run jobs. + +## Detailed File Overviews +- `pubspec.yaml`: Defines dependencies, notably the `freezed` and `json_serializable` packages for code generation. +- `analysis_options.yaml`: Standardized linting rules for the Dart package. +- `CHANGELOG.md`: Tracks version changes of the data models. + +## Subdirectories +- `lib/`: Contains the exported model definitions. + - `src/models/`: The actual implementation of domain models (e.g., `LatteOrder`, `Barista`, `Machine`) using `freezed` for immutability and JSON support. +- `test/`: Unit tests for model serialization and business logic contained within the models. + +## Implementation Details +- **Architecture**: A logic-only Dart package without any dependency on the Flutter framework, making it suitable for both frontend and backend use. +- **Code Generation**: Uses `build_runner` to synthesize the `*.freezed.dart` and `*.g.dart` files. Run `dart run build_runner build` to regenerate these files after model changes. + +## Dependencies/Relationships +- **Frontend:** Imported by `genlatte-ui` to handle UI state and API responses. +- **Backend:** Used by Cloud Functions and Cloud Run services to ensure data integrity when interacting with Firestore and external APIs. diff --git a/genlatte/genlatte-data/README.md b/genlatte/genlatte-data/README.md new file mode 100644 index 0000000..8831761 --- /dev/null +++ b/genlatte/genlatte-data/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/genlatte/genlatte-data/analysis_options.yaml b/genlatte/genlatte-data/analysis_options.yaml new file mode 100644 index 0000000..fcf0b22 --- /dev/null +++ b/genlatte/genlatte-data/analysis_options.yaml @@ -0,0 +1,17 @@ +include: package:very_good_analysis/analysis_options.yaml +linter: + rules: + public_member_api_docs: true + omit_local_variable_types: false + document_ignores: true + +analyzer: + errors: + todo: ignore + document_ignores: ignore + exclude: + - "**.freezed.dart" + - "**.g.dart" + +formatter: + trailing_commas: preserve \ No newline at end of file diff --git a/genlatte/genlatte-data/lib/CONTEXT.md b/genlatte/genlatte-data/lib/CONTEXT.md new file mode 100644 index 0000000..7cdcac8 --- /dev/null +++ b/genlatte/genlatte-data/lib/CONTEXT.md @@ -0,0 +1,4 @@ +# genlatte-data Lib Root + +**Purpose:** +Base logical entrypoint encapsulating `models/` mapping schemas. diff --git a/genlatte/genlatte-data/lib/models.dart b/genlatte/genlatte-data/lib/models.dart new file mode 100644 index 0000000..a78c9ab --- /dev/null +++ b/genlatte/genlatte-data/lib/models.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/models/models.dart'; diff --git a/genlatte/genlatte-data/lib/src/CONTEXT.md b/genlatte/genlatte-data/lib/src/CONTEXT.md new file mode 100644 index 0000000..31d2f86 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/CONTEXT.md @@ -0,0 +1,7 @@ +# Shared Data Package Sub-Tree + +**Purpose:** +A structural grouping boundary for `genlatte_data`. + +**Implementation Details:** +- **Navigation**: All critical logic resides deeply within `models/`, representing universal schema boundaries across the different systems. diff --git a/genlatte/genlatte-data/lib/src/models/CONTEXT.md b/genlatte/genlatte-data/lib/src/models/CONTEXT.md new file mode 100644 index 0000000..5f2d7e3 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/CONTEXT.md @@ -0,0 +1,28 @@ +# Shared Models + +**Purpose:** +Provides pure Dart representations of the NoSQL Firebase document structures. Ensures that all applications (frontend, backend scripts, emulators) share exactly the same data boundaries and serialization logic using `freezed` and `json_serializable`. + +**Detailed File Overviews:** + +- `models.dart`: + - **Description**: Barrel export file. + +- `latte_order.dart`: + - **Description**: The core foundational entity representing a Customer's Order. + - **Core Logic**: Contains nested architectures (`Latte`, `LatteOrder`, and `LatteOrderMetadata`). Generates JSON mapping logic tracking properties like `id`, `name`, `status`, `imageUrl`, and timestamps `orderSubmittedTime` / `completionTime`. + +- `latte_image.dart` / `latte_options.dart`: + - **Description**: Defines visual configuration options for image generation (temperature, seeds, and image URLs). + +- `machine.dart`: + - **Description**: Represents the physical printing hardware located dynamically around the event floor. + +- `question.dart`: + - **Description**: Models structures for the visual Questionnaire ("Is it sunny?", "How do you feel?") utilized inside the Kiosk wizard flow. + +- `barista.dart`: + - **Description**: Encapsulates metadata specifically relating to the employee actively fulfilling orders. + +**Dependencies/Relationships:** +- Serves as the ultimate source of truth for schema validation across the `genlatte_data` package. Generated via `build_runner`. diff --git a/genlatte/genlatte-data/lib/src/models/barista.dart b/genlatte/genlatte-data/lib/src/models/barista.dart new file mode 100644 index 0000000..b7ed861 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/barista.dart @@ -0,0 +1,83 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:data_layer/data_layer.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'barista.freezed.dart'; +part 'barista.g.dart'; + +/// An admin worker can moderate and fulfill orders. +@freezed +abstract class Barista with _$Barista { + /// Instantiates a [Barista]. + const factory Barista({ + required String username, + required BaristaPersona persona, + String? id, + }) = _Barista; + + /// Json deserializer for [Barista]. + factory Barista.fromJson(Map json) => + _$BaristaFromJson(json); + + /// Data layer bindings for [Barista]. + static Bindings bindings = Bindings( + fromJson: Barista.fromJson, + toJson: (obj) => obj.toJson(), + getId: (item) => item.id, + getDetailUrl: (id) => ApiUrl(path: 'baristas/$id'), + getListUrl: () => const ApiUrl(path: 'baristas'), + ); +} + +/// Generic avatars for [Barista]s. +enum BaristaPersona { + /// A Black female barista. + blackFemale, + + /// An Asian female barista. + asianFemale, + + /// A Caucasian female barista. + caucasianFemale, + + /// A Hispanic female barista. + hispanicFemale, + + /// An Indian female barista. + indianFemale, + + /// A Black male barista. + blackMale, + + /// An Asian male barista. + asianMale, + + /// A Caucasian male barista. + caucasianMale, + + /// A Hispanic male barista. + hispanicMale, + + /// An Indian male barista. + indianMale; + + /// The asset name for the avatar image. + String get assetName => 'assets/avatars/$name.png'; +} + +/// Extension for [Barista] lists. +extension BaristaList on List { + /// Converts the list of orders into a map keyed by order id. + Map toIdsMap() { + return fold>( + {}, + (map, order) { + map[order.id!] = order; + return map; + }, + ); + } +} diff --git a/genlatte/genlatte-data/lib/src/models/barista.freezed.dart b/genlatte/genlatte-data/lib/src/models/barista.freezed.dart new file mode 100644 index 0000000..8bcbdb2 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/barista.freezed.dart @@ -0,0 +1,287 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'barista.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$Barista { + + String get username; BaristaPersona get persona; String? get id; +/// Create a copy of Barista +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BaristaCopyWith get copyWith => _$BaristaCopyWithImpl(this as Barista, _$identity); + + /// Serializes this Barista to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Barista&&(identical(other.username, username) || other.username == username)&&(identical(other.persona, persona) || other.persona == persona)&&(identical(other.id, id) || other.id == id)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,username,persona,id); + +@override +String toString() { + return 'Barista(username: $username, persona: $persona, id: $id)'; +} + + +} + +/// @nodoc +abstract mixin class $BaristaCopyWith<$Res> { + factory $BaristaCopyWith(Barista value, $Res Function(Barista) _then) = _$BaristaCopyWithImpl; +@useResult +$Res call({ + String username, BaristaPersona persona, String? id +}); + + + + +} +/// @nodoc +class _$BaristaCopyWithImpl<$Res> + implements $BaristaCopyWith<$Res> { + _$BaristaCopyWithImpl(this._self, this._then); + + final Barista _self; + final $Res Function(Barista) _then; + +/// Create a copy of Barista +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? username = null,Object? persona = null,Object? id = freezed,}) { + return _then(_self.copyWith( +username: null == username ? _self.username : username // ignore: cast_nullable_to_non_nullable +as String,persona: null == persona ? _self.persona : persona // ignore: cast_nullable_to_non_nullable +as BaristaPersona,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [Barista]. +extension BaristaPatterns on Barista { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Barista value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Barista() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Barista value) $default,){ +final _that = this; +switch (_that) { +case _Barista(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Barista value)? $default,){ +final _that = this; +switch (_that) { +case _Barista() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String username, BaristaPersona persona, String? id)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Barista() when $default != null: +return $default(_that.username,_that.persona,_that.id);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String username, BaristaPersona persona, String? id) $default,) {final _that = this; +switch (_that) { +case _Barista(): +return $default(_that.username,_that.persona,_that.id);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String username, BaristaPersona persona, String? id)? $default,) {final _that = this; +switch (_that) { +case _Barista() when $default != null: +return $default(_that.username,_that.persona,_that.id);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _Barista implements Barista { + const _Barista({required this.username, required this.persona, this.id}); + factory _Barista.fromJson(Map json) => _$BaristaFromJson(json); + +@override final String username; +@override final BaristaPersona persona; +@override final String? id; + +/// Create a copy of Barista +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$BaristaCopyWith<_Barista> get copyWith => __$BaristaCopyWithImpl<_Barista>(this, _$identity); + +@override +Map toJson() { + return _$BaristaToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Barista&&(identical(other.username, username) || other.username == username)&&(identical(other.persona, persona) || other.persona == persona)&&(identical(other.id, id) || other.id == id)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,username,persona,id); + +@override +String toString() { + return 'Barista(username: $username, persona: $persona, id: $id)'; +} + + +} + +/// @nodoc +abstract mixin class _$BaristaCopyWith<$Res> implements $BaristaCopyWith<$Res> { + factory _$BaristaCopyWith(_Barista value, $Res Function(_Barista) _then) = __$BaristaCopyWithImpl; +@override @useResult +$Res call({ + String username, BaristaPersona persona, String? id +}); + + + + +} +/// @nodoc +class __$BaristaCopyWithImpl<$Res> + implements _$BaristaCopyWith<$Res> { + __$BaristaCopyWithImpl(this._self, this._then); + + final _Barista _self; + final $Res Function(_Barista) _then; + +/// Create a copy of Barista +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? username = null,Object? persona = null,Object? id = freezed,}) { + return _then(_Barista( +username: null == username ? _self.username : username // ignore: cast_nullable_to_non_nullable +as String,persona: null == persona ? _self.persona : persona // ignore: cast_nullable_to_non_nullable +as BaristaPersona,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-data/lib/src/models/barista.g.dart b/genlatte/genlatte-data/lib/src/models/barista.g.dart new file mode 100644 index 0000000..04fcf40 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/barista.g.dart @@ -0,0 +1,36 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'barista.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_Barista _$BaristaFromJson(Map json) => _Barista( + username: json['username'] as String, + persona: $enumDecode(_$BaristaPersonaEnumMap, json['persona']), + id: json['id'] as String?, +); + +Map _$BaristaToJson(_Barista instance) => { + 'username': instance.username, + 'persona': _$BaristaPersonaEnumMap[instance.persona]!, + 'id': instance.id, +}; + +const _$BaristaPersonaEnumMap = { + BaristaPersona.blackFemale: 'blackFemale', + BaristaPersona.asianFemale: 'asianFemale', + BaristaPersona.caucasianFemale: 'caucasianFemale', + BaristaPersona.hispanicFemale: 'hispanicFemale', + BaristaPersona.indianFemale: 'indianFemale', + BaristaPersona.blackMale: 'blackMale', + BaristaPersona.asianMale: 'asianMale', + BaristaPersona.caucasianMale: 'caucasianMale', + BaristaPersona.hispanicMale: 'hispanicMale', + BaristaPersona.indianMale: 'indianMale', +}; diff --git a/genlatte/genlatte-data/lib/src/models/latte_image.dart b/genlatte/genlatte-data/lib/src/models/latte_image.dart new file mode 100644 index 0000000..421583e --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/latte_image.dart @@ -0,0 +1,218 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport 'package:genlatte_data/src/models/latte_order.dart'; +library; + +import 'package:data_layer/api.dart'; +import 'package:data_layer/sources.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte_data/models.dart' show Question, QuestionConverter; + +part 'latte_image.freezed.dart'; +part 'latte_image.g.dart'; + +/// A set of 4 [LatteImage]s created for a [LatteOrder], to be viewed together. +@freezed +abstract class LatteImageBatch with _$LatteImageBatch { + /// Instantiates a [LatteImageBatch]. + const factory LatteImageBatch({ + required String id, + required String orderId, + @LatteImageConverter() required LatteImage? image0, + @LatteImageConverter() required LatteImage? image1, + @LatteImageConverter() required LatteImage? image2, + @LatteImageConverter() required LatteImage? image3, + @LatteImageBatchParentConverter() LatteImageBatchParent? parent, + }) = _LatteImageBatch; + + const LatteImageBatch._(); + + /// Json deserializer for [LatteImageBatch]. + factory LatteImageBatch.fromJson(Map json) => + _$LatteImageBatchFromJson(json); + + /// Iterates over all images. + Iterable get images sync* { + yield image0; + yield image1; + yield image2; + yield image3; + } + + /// Returns the image at the given index. + LatteImage? operator [](int index) => switch (index) { + 0 => image0, + 1 => image1, + 2 => image2, + 3 => image3, + _ => throw RangeError('Invalid image index: $index'), + }; + + /// Data Layer bindings for [LatteImageBatch]. + static Bindings bindings = Bindings( + fromJson: LatteImageBatch.fromJson, + toJson: (obj) => obj.toJson(), + getId: (obj) => obj.id, + getDetailUrl: (id) => ApiUrl(path: 'latteImageBatches/$id'), + getListUrl: () => const ApiUrl(path: 'latteImageBatches'), + ); + + /// Returns a copy of the [LatteImageBatch] with the questions for the + /// selected image updated. + LatteImageBatch answerQuestion( + int index, + Question question, + Object? answer, + ) => switch (index) { + 0 => copyWith(image0: image0!.answerQuestion(question, answer)), + 1 => copyWith(image1: image1!.answerQuestion(question, answer)), + 2 => copyWith(image2: image2!.answerQuestion(question, answer)), + 3 => copyWith(image3: image3!.answerQuestion(question, answer)), + _ => throw RangeError('Invalid image index: $index'), + }; +} + +@freezed +/// Pointers toward the [LatteImageBatch] and specific image field that this +/// image batch forked from. +abstract class LatteImageBatchParent with _$LatteImageBatchParent { + /// Instantiates a [LatteImageBatchParent]. + const factory LatteImageBatchParent({ + /// Firebase Id of the [LatteImageBatch] that this image batch forked from. + required String id, + + /// "image0", "image1", "image2", or "image3". No other values are + /// acceptable. + required String imageIndex, + }) = _LatteImageBatchParent; + + /// Json deserializer for [LatteImageBatchParent]. + factory LatteImageBatchParent.fromJson(Map json) => + _$LatteImageBatchParentFromJson(json); +} + +/// Candidate latte art generated by Nanobanana. +@freezed +abstract class LatteImage with _$LatteImage { + /// Creates a new [LatteImage]. + const factory LatteImage({ + /// The URL of the generated image. + required String imageUrl, + + /// The upgraded prompt sent to Gemini. + required String prompt, + + @QuestionConverter() required List? questions, + + /// A description of the generated image. + required String description, + }) = _LatteImage; + + const LatteImage._(); + + /// Json deserializer for [LatteImage]. + factory LatteImage.fromJson(Map json) => + _$LatteImageFromJson(json); + + /// Returns a copy of the [LatteImage] with given question answered. + LatteImage answerQuestion(Question question, Object? answer) { + assert( + questions != null && questions!.isNotEmpty, + 'Cannot answer questions on a LatteImage without any.', + ); + final questionCopies = List.from(questions!); + for (final (index, q) in questionCopies.indexed) { + if (q.id == question.id) { + questionCopies[index] = q.copyWithAnswer(answer); + return copyWith(questions: questionCopies); + } + } + throw Exception('Question ${question.id} not found in LatteImage.'); + } +} + +/// Candidate latte art generated by Nanobanana. +@freezed +abstract class RecentLatteImage with _$RecentLatteImage { + /// Creates a new [LatteImage]. + const factory RecentLatteImage({ + /// The URL of the generated image. + required String imageUrl, + + /// The upgraded prompt sent to Gemini. + required String prompt, + + /// The happy place that was used to generate the image. + required String happyPlace, + + /// A description of the generated image. + required String description, + + /// The name of the user who created this image. + required String name, + + /// The time the originating [LatteOrder] was submitted and this document + /// was created. + required DateTime createdAt, + + /// The ID of the generated image. + String? id, + }) = _RecentLatteImage; + + const RecentLatteImage._(); + + /// Json deserializer for [RecentLatteImage]. + factory RecentLatteImage.fromJson(Map json) => + _$RecentLatteImageFromJson(json); + + /// Data Layer bindings for [RecentLatteImage] as recent images. + static Bindings recentBindings = Bindings( + fromJson: (data) { + // When objects are brand new, they can briefly be emitted before the + // server actually assigns a `createdAt` value. However, that missing + // value will be very close to `now`, so that is a suitable fallback. + final Object? createdAt = data['createdAt']; + if (createdAt == null || (createdAt is String && createdAt.isEmpty)) { + data['createdAt'] = DateTime.now().toUtc().toIso8601String(); + } + return RecentLatteImage.fromJson(data); + }, + toJson: (obj) => obj.toJson(), + getId: (obj) => obj.id, + getDetailUrl: (id) => ApiUrl(path: 'recentLatteImages/$id'), + getListUrl: () => const ApiUrl(path: 'recentLatteImages'), + ); +} + +/// {@template LatteImageConverter} +/// Json converter for [LatteImage]. +/// {@endtemplate} +class LatteImageConverter + extends JsonConverter> { + /// {@macro LatteImageConverter} + const LatteImageConverter(); + + @override + LatteImage fromJson(Map json) => LatteImage.fromJson(json); + + @override + Map toJson(LatteImage object) => object.toJson(); +} + +/// {@template LatteImageBatchParentConverter} +/// Json converter for [LatteImageBatchParent]. +/// {@endtemplate} +class LatteImageBatchParentConverter + extends JsonConverter> { + /// {@macro LatteImageBatchParentConverter} + const LatteImageBatchParentConverter(); + + @override + LatteImageBatchParent fromJson(Map json) => + LatteImageBatchParent.fromJson(json); + + @override + Map toJson(LatteImageBatchParent object) => object.toJson(); +} diff --git a/genlatte/genlatte-data/lib/src/models/latte_image.freezed.dart b/genlatte/genlatte-data/lib/src/models/latte_image.freezed.dart new file mode 100644 index 0000000..eea4864 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/latte_image.freezed.dart @@ -0,0 +1,1274 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'latte_image.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$LatteImageBatch { + + String get id; String get orderId;@LatteImageConverter() LatteImage? get image0;@LatteImageConverter() LatteImage? get image1;@LatteImageConverter() LatteImage? get image2;@LatteImageConverter() LatteImage? get image3;@LatteImageBatchParentConverter() LatteImageBatchParent? get parent; +/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LatteImageBatchCopyWith get copyWith => _$LatteImageBatchCopyWithImpl(this as LatteImageBatch, _$identity); + + /// Serializes this LatteImageBatch to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LatteImageBatch&&(identical(other.id, id) || other.id == id)&&(identical(other.orderId, orderId) || other.orderId == orderId)&&(identical(other.image0, image0) || other.image0 == image0)&&(identical(other.image1, image1) || other.image1 == image1)&&(identical(other.image2, image2) || other.image2 == image2)&&(identical(other.image3, image3) || other.image3 == image3)&&(identical(other.parent, parent) || other.parent == parent)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,orderId,image0,image1,image2,image3,parent); + +@override +String toString() { + return 'LatteImageBatch(id: $id, orderId: $orderId, image0: $image0, image1: $image1, image2: $image2, image3: $image3, parent: $parent)'; +} + + +} + +/// @nodoc +abstract mixin class $LatteImageBatchCopyWith<$Res> { + factory $LatteImageBatchCopyWith(LatteImageBatch value, $Res Function(LatteImageBatch) _then) = _$LatteImageBatchCopyWithImpl; +@useResult +$Res call({ + String id, String orderId,@LatteImageConverter() LatteImage? image0,@LatteImageConverter() LatteImage? image1,@LatteImageConverter() LatteImage? image2,@LatteImageConverter() LatteImage? image3,@LatteImageBatchParentConverter() LatteImageBatchParent? parent +}); + + +$LatteImageCopyWith<$Res>? get image0;$LatteImageCopyWith<$Res>? get image1;$LatteImageCopyWith<$Res>? get image2;$LatteImageCopyWith<$Res>? get image3;$LatteImageBatchParentCopyWith<$Res>? get parent; + +} +/// @nodoc +class _$LatteImageBatchCopyWithImpl<$Res> + implements $LatteImageBatchCopyWith<$Res> { + _$LatteImageBatchCopyWithImpl(this._self, this._then); + + final LatteImageBatch _self; + final $Res Function(LatteImageBatch) _then; + +/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? orderId = null,Object? image0 = freezed,Object? image1 = freezed,Object? image2 = freezed,Object? image3 = freezed,Object? parent = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,orderId: null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String,image0: freezed == image0 ? _self.image0 : image0 // ignore: cast_nullable_to_non_nullable +as LatteImage?,image1: freezed == image1 ? _self.image1 : image1 // ignore: cast_nullable_to_non_nullable +as LatteImage?,image2: freezed == image2 ? _self.image2 : image2 // ignore: cast_nullable_to_non_nullable +as LatteImage?,image3: freezed == image3 ? _self.image3 : image3 // ignore: cast_nullable_to_non_nullable +as LatteImage?,parent: freezed == parent ? _self.parent : parent // ignore: cast_nullable_to_non_nullable +as LatteImageBatchParent?, + )); +} +/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageCopyWith<$Res>? get image0 { + if (_self.image0 == null) { + return null; + } + + return $LatteImageCopyWith<$Res>(_self.image0!, (value) { + return _then(_self.copyWith(image0: value)); + }); +}/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageCopyWith<$Res>? get image1 { + if (_self.image1 == null) { + return null; + } + + return $LatteImageCopyWith<$Res>(_self.image1!, (value) { + return _then(_self.copyWith(image1: value)); + }); +}/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageCopyWith<$Res>? get image2 { + if (_self.image2 == null) { + return null; + } + + return $LatteImageCopyWith<$Res>(_self.image2!, (value) { + return _then(_self.copyWith(image2: value)); + }); +}/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageCopyWith<$Res>? get image3 { + if (_self.image3 == null) { + return null; + } + + return $LatteImageCopyWith<$Res>(_self.image3!, (value) { + return _then(_self.copyWith(image3: value)); + }); +}/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageBatchParentCopyWith<$Res>? get parent { + if (_self.parent == null) { + return null; + } + + return $LatteImageBatchParentCopyWith<$Res>(_self.parent!, (value) { + return _then(_self.copyWith(parent: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [LatteImageBatch]. +extension LatteImageBatchPatterns on LatteImageBatch { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LatteImageBatch value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LatteImageBatch() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LatteImageBatch value) $default,){ +final _that = this; +switch (_that) { +case _LatteImageBatch(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LatteImageBatch value)? $default,){ +final _that = this; +switch (_that) { +case _LatteImageBatch() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String orderId, @LatteImageConverter() LatteImage? image0, @LatteImageConverter() LatteImage? image1, @LatteImageConverter() LatteImage? image2, @LatteImageConverter() LatteImage? image3, @LatteImageBatchParentConverter() LatteImageBatchParent? parent)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LatteImageBatch() when $default != null: +return $default(_that.id,_that.orderId,_that.image0,_that.image1,_that.image2,_that.image3,_that.parent);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String orderId, @LatteImageConverter() LatteImage? image0, @LatteImageConverter() LatteImage? image1, @LatteImageConverter() LatteImage? image2, @LatteImageConverter() LatteImage? image3, @LatteImageBatchParentConverter() LatteImageBatchParent? parent) $default,) {final _that = this; +switch (_that) { +case _LatteImageBatch(): +return $default(_that.id,_that.orderId,_that.image0,_that.image1,_that.image2,_that.image3,_that.parent);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String orderId, @LatteImageConverter() LatteImage? image0, @LatteImageConverter() LatteImage? image1, @LatteImageConverter() LatteImage? image2, @LatteImageConverter() LatteImage? image3, @LatteImageBatchParentConverter() LatteImageBatchParent? parent)? $default,) {final _that = this; +switch (_that) { +case _LatteImageBatch() when $default != null: +return $default(_that.id,_that.orderId,_that.image0,_that.image1,_that.image2,_that.image3,_that.parent);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _LatteImageBatch extends LatteImageBatch { + const _LatteImageBatch({required this.id, required this.orderId, @LatteImageConverter() required this.image0, @LatteImageConverter() required this.image1, @LatteImageConverter() required this.image2, @LatteImageConverter() required this.image3, @LatteImageBatchParentConverter() this.parent}): super._(); + factory _LatteImageBatch.fromJson(Map json) => _$LatteImageBatchFromJson(json); + +@override final String id; +@override final String orderId; +@override@LatteImageConverter() final LatteImage? image0; +@override@LatteImageConverter() final LatteImage? image1; +@override@LatteImageConverter() final LatteImage? image2; +@override@LatteImageConverter() final LatteImage? image3; +@override@LatteImageBatchParentConverter() final LatteImageBatchParent? parent; + +/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LatteImageBatchCopyWith<_LatteImageBatch> get copyWith => __$LatteImageBatchCopyWithImpl<_LatteImageBatch>(this, _$identity); + +@override +Map toJson() { + return _$LatteImageBatchToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LatteImageBatch&&(identical(other.id, id) || other.id == id)&&(identical(other.orderId, orderId) || other.orderId == orderId)&&(identical(other.image0, image0) || other.image0 == image0)&&(identical(other.image1, image1) || other.image1 == image1)&&(identical(other.image2, image2) || other.image2 == image2)&&(identical(other.image3, image3) || other.image3 == image3)&&(identical(other.parent, parent) || other.parent == parent)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,orderId,image0,image1,image2,image3,parent); + +@override +String toString() { + return 'LatteImageBatch(id: $id, orderId: $orderId, image0: $image0, image1: $image1, image2: $image2, image3: $image3, parent: $parent)'; +} + + +} + +/// @nodoc +abstract mixin class _$LatteImageBatchCopyWith<$Res> implements $LatteImageBatchCopyWith<$Res> { + factory _$LatteImageBatchCopyWith(_LatteImageBatch value, $Res Function(_LatteImageBatch) _then) = __$LatteImageBatchCopyWithImpl; +@override @useResult +$Res call({ + String id, String orderId,@LatteImageConverter() LatteImage? image0,@LatteImageConverter() LatteImage? image1,@LatteImageConverter() LatteImage? image2,@LatteImageConverter() LatteImage? image3,@LatteImageBatchParentConverter() LatteImageBatchParent? parent +}); + + +@override $LatteImageCopyWith<$Res>? get image0;@override $LatteImageCopyWith<$Res>? get image1;@override $LatteImageCopyWith<$Res>? get image2;@override $LatteImageCopyWith<$Res>? get image3;@override $LatteImageBatchParentCopyWith<$Res>? get parent; + +} +/// @nodoc +class __$LatteImageBatchCopyWithImpl<$Res> + implements _$LatteImageBatchCopyWith<$Res> { + __$LatteImageBatchCopyWithImpl(this._self, this._then); + + final _LatteImageBatch _self; + final $Res Function(_LatteImageBatch) _then; + +/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? orderId = null,Object? image0 = freezed,Object? image1 = freezed,Object? image2 = freezed,Object? image3 = freezed,Object? parent = freezed,}) { + return _then(_LatteImageBatch( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,orderId: null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String,image0: freezed == image0 ? _self.image0 : image0 // ignore: cast_nullable_to_non_nullable +as LatteImage?,image1: freezed == image1 ? _self.image1 : image1 // ignore: cast_nullable_to_non_nullable +as LatteImage?,image2: freezed == image2 ? _self.image2 : image2 // ignore: cast_nullable_to_non_nullable +as LatteImage?,image3: freezed == image3 ? _self.image3 : image3 // ignore: cast_nullable_to_non_nullable +as LatteImage?,parent: freezed == parent ? _self.parent : parent // ignore: cast_nullable_to_non_nullable +as LatteImageBatchParent?, + )); +} + +/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageCopyWith<$Res>? get image0 { + if (_self.image0 == null) { + return null; + } + + return $LatteImageCopyWith<$Res>(_self.image0!, (value) { + return _then(_self.copyWith(image0: value)); + }); +}/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageCopyWith<$Res>? get image1 { + if (_self.image1 == null) { + return null; + } + + return $LatteImageCopyWith<$Res>(_self.image1!, (value) { + return _then(_self.copyWith(image1: value)); + }); +}/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageCopyWith<$Res>? get image2 { + if (_self.image2 == null) { + return null; + } + + return $LatteImageCopyWith<$Res>(_self.image2!, (value) { + return _then(_self.copyWith(image2: value)); + }); +}/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageCopyWith<$Res>? get image3 { + if (_self.image3 == null) { + return null; + } + + return $LatteImageCopyWith<$Res>(_self.image3!, (value) { + return _then(_self.copyWith(image3: value)); + }); +}/// Create a copy of LatteImageBatch +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageBatchParentCopyWith<$Res>? get parent { + if (_self.parent == null) { + return null; + } + + return $LatteImageBatchParentCopyWith<$Res>(_self.parent!, (value) { + return _then(_self.copyWith(parent: value)); + }); +} +} + + +/// @nodoc +mixin _$LatteImageBatchParent { + +/// Firebase Id of the [LatteImageBatch] that this image batch forked from. + String get id;/// "image0", "image1", "image2", or "image3". No other values are +/// acceptable. + String get imageIndex; +/// Create a copy of LatteImageBatchParent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LatteImageBatchParentCopyWith get copyWith => _$LatteImageBatchParentCopyWithImpl(this as LatteImageBatchParent, _$identity); + + /// Serializes this LatteImageBatchParent to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LatteImageBatchParent&&(identical(other.id, id) || other.id == id)&&(identical(other.imageIndex, imageIndex) || other.imageIndex == imageIndex)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,imageIndex); + +@override +String toString() { + return 'LatteImageBatchParent(id: $id, imageIndex: $imageIndex)'; +} + + +} + +/// @nodoc +abstract mixin class $LatteImageBatchParentCopyWith<$Res> { + factory $LatteImageBatchParentCopyWith(LatteImageBatchParent value, $Res Function(LatteImageBatchParent) _then) = _$LatteImageBatchParentCopyWithImpl; +@useResult +$Res call({ + String id, String imageIndex +}); + + + + +} +/// @nodoc +class _$LatteImageBatchParentCopyWithImpl<$Res> + implements $LatteImageBatchParentCopyWith<$Res> { + _$LatteImageBatchParentCopyWithImpl(this._self, this._then); + + final LatteImageBatchParent _self; + final $Res Function(LatteImageBatchParent) _then; + +/// Create a copy of LatteImageBatchParent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? imageIndex = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,imageIndex: null == imageIndex ? _self.imageIndex : imageIndex // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [LatteImageBatchParent]. +extension LatteImageBatchParentPatterns on LatteImageBatchParent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LatteImageBatchParent value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LatteImageBatchParent() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LatteImageBatchParent value) $default,){ +final _that = this; +switch (_that) { +case _LatteImageBatchParent(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LatteImageBatchParent value)? $default,){ +final _that = this; +switch (_that) { +case _LatteImageBatchParent() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String imageIndex)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LatteImageBatchParent() when $default != null: +return $default(_that.id,_that.imageIndex);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String imageIndex) $default,) {final _that = this; +switch (_that) { +case _LatteImageBatchParent(): +return $default(_that.id,_that.imageIndex);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String imageIndex)? $default,) {final _that = this; +switch (_that) { +case _LatteImageBatchParent() when $default != null: +return $default(_that.id,_that.imageIndex);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _LatteImageBatchParent implements LatteImageBatchParent { + const _LatteImageBatchParent({required this.id, required this.imageIndex}); + factory _LatteImageBatchParent.fromJson(Map json) => _$LatteImageBatchParentFromJson(json); + +/// Firebase Id of the [LatteImageBatch] that this image batch forked from. +@override final String id; +/// "image0", "image1", "image2", or "image3". No other values are +/// acceptable. +@override final String imageIndex; + +/// Create a copy of LatteImageBatchParent +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LatteImageBatchParentCopyWith<_LatteImageBatchParent> get copyWith => __$LatteImageBatchParentCopyWithImpl<_LatteImageBatchParent>(this, _$identity); + +@override +Map toJson() { + return _$LatteImageBatchParentToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LatteImageBatchParent&&(identical(other.id, id) || other.id == id)&&(identical(other.imageIndex, imageIndex) || other.imageIndex == imageIndex)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,imageIndex); + +@override +String toString() { + return 'LatteImageBatchParent(id: $id, imageIndex: $imageIndex)'; +} + + +} + +/// @nodoc +abstract mixin class _$LatteImageBatchParentCopyWith<$Res> implements $LatteImageBatchParentCopyWith<$Res> { + factory _$LatteImageBatchParentCopyWith(_LatteImageBatchParent value, $Res Function(_LatteImageBatchParent) _then) = __$LatteImageBatchParentCopyWithImpl; +@override @useResult +$Res call({ + String id, String imageIndex +}); + + + + +} +/// @nodoc +class __$LatteImageBatchParentCopyWithImpl<$Res> + implements _$LatteImageBatchParentCopyWith<$Res> { + __$LatteImageBatchParentCopyWithImpl(this._self, this._then); + + final _LatteImageBatchParent _self; + final $Res Function(_LatteImageBatchParent) _then; + +/// Create a copy of LatteImageBatchParent +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? imageIndex = null,}) { + return _then(_LatteImageBatchParent( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,imageIndex: null == imageIndex ? _self.imageIndex : imageIndex // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + + +/// @nodoc +mixin _$LatteImage { + +/// The URL of the generated image. + String get imageUrl;/// The upgraded prompt sent to Gemini. + String get prompt;@QuestionConverter() List? get questions;/// A description of the generated image. + String get description; +/// Create a copy of LatteImage +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LatteImageCopyWith get copyWith => _$LatteImageCopyWithImpl(this as LatteImage, _$identity); + + /// Serializes this LatteImage to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LatteImage&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.prompt, prompt) || other.prompt == prompt)&&const DeepCollectionEquality().equals(other.questions, questions)&&(identical(other.description, description) || other.description == description)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,imageUrl,prompt,const DeepCollectionEquality().hash(questions),description); + +@override +String toString() { + return 'LatteImage(imageUrl: $imageUrl, prompt: $prompt, questions: $questions, description: $description)'; +} + + +} + +/// @nodoc +abstract mixin class $LatteImageCopyWith<$Res> { + factory $LatteImageCopyWith(LatteImage value, $Res Function(LatteImage) _then) = _$LatteImageCopyWithImpl; +@useResult +$Res call({ + String imageUrl, String prompt,@QuestionConverter() List? questions, String description +}); + + + + +} +/// @nodoc +class _$LatteImageCopyWithImpl<$Res> + implements $LatteImageCopyWith<$Res> { + _$LatteImageCopyWithImpl(this._self, this._then); + + final LatteImage _self; + final $Res Function(LatteImage) _then; + +/// Create a copy of LatteImage +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? imageUrl = null,Object? prompt = null,Object? questions = freezed,Object? description = null,}) { + return _then(_self.copyWith( +imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String,prompt: null == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String,questions: freezed == questions ? _self.questions : questions // ignore: cast_nullable_to_non_nullable +as List?,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [LatteImage]. +extension LatteImagePatterns on LatteImage { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LatteImage value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LatteImage() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LatteImage value) $default,){ +final _that = this; +switch (_that) { +case _LatteImage(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LatteImage value)? $default,){ +final _that = this; +switch (_that) { +case _LatteImage() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String imageUrl, String prompt, @QuestionConverter() List? questions, String description)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LatteImage() when $default != null: +return $default(_that.imageUrl,_that.prompt,_that.questions,_that.description);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String imageUrl, String prompt, @QuestionConverter() List? questions, String description) $default,) {final _that = this; +switch (_that) { +case _LatteImage(): +return $default(_that.imageUrl,_that.prompt,_that.questions,_that.description);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String imageUrl, String prompt, @QuestionConverter() List? questions, String description)? $default,) {final _that = this; +switch (_that) { +case _LatteImage() when $default != null: +return $default(_that.imageUrl,_that.prompt,_that.questions,_that.description);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _LatteImage extends LatteImage { + const _LatteImage({required this.imageUrl, required this.prompt, @QuestionConverter() required final List? questions, required this.description}): _questions = questions,super._(); + factory _LatteImage.fromJson(Map json) => _$LatteImageFromJson(json); + +/// The URL of the generated image. +@override final String imageUrl; +/// The upgraded prompt sent to Gemini. +@override final String prompt; + final List? _questions; +@override@QuestionConverter() List? get questions { + final value = _questions; + if (value == null) return null; + if (_questions is EqualUnmodifiableListView) return _questions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + +/// A description of the generated image. +@override final String description; + +/// Create a copy of LatteImage +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LatteImageCopyWith<_LatteImage> get copyWith => __$LatteImageCopyWithImpl<_LatteImage>(this, _$identity); + +@override +Map toJson() { + return _$LatteImageToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LatteImage&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.prompt, prompt) || other.prompt == prompt)&&const DeepCollectionEquality().equals(other._questions, _questions)&&(identical(other.description, description) || other.description == description)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,imageUrl,prompt,const DeepCollectionEquality().hash(_questions),description); + +@override +String toString() { + return 'LatteImage(imageUrl: $imageUrl, prompt: $prompt, questions: $questions, description: $description)'; +} + + +} + +/// @nodoc +abstract mixin class _$LatteImageCopyWith<$Res> implements $LatteImageCopyWith<$Res> { + factory _$LatteImageCopyWith(_LatteImage value, $Res Function(_LatteImage) _then) = __$LatteImageCopyWithImpl; +@override @useResult +$Res call({ + String imageUrl, String prompt,@QuestionConverter() List? questions, String description +}); + + + + +} +/// @nodoc +class __$LatteImageCopyWithImpl<$Res> + implements _$LatteImageCopyWith<$Res> { + __$LatteImageCopyWithImpl(this._self, this._then); + + final _LatteImage _self; + final $Res Function(_LatteImage) _then; + +/// Create a copy of LatteImage +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? imageUrl = null,Object? prompt = null,Object? questions = freezed,Object? description = null,}) { + return _then(_LatteImage( +imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String,prompt: null == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String,questions: freezed == questions ? _self._questions : questions // ignore: cast_nullable_to_non_nullable +as List?,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + + +/// @nodoc +mixin _$RecentLatteImage { + +/// The URL of the generated image. + String get imageUrl;/// The upgraded prompt sent to Gemini. + String get prompt;/// The happy place that was used to generate the image. + String get happyPlace;/// A description of the generated image. + String get description;/// The name of the user who created this image. + String get name;/// The time the originating [LatteOrder] was submitted and this document +/// was created. + DateTime get createdAt;/// The ID of the generated image. + String? get id; +/// Create a copy of RecentLatteImage +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$RecentLatteImageCopyWith get copyWith => _$RecentLatteImageCopyWithImpl(this as RecentLatteImage, _$identity); + + /// Serializes this RecentLatteImage to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RecentLatteImage&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.prompt, prompt) || other.prompt == prompt)&&(identical(other.happyPlace, happyPlace) || other.happyPlace == happyPlace)&&(identical(other.description, description) || other.description == description)&&(identical(other.name, name) || other.name == name)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.id, id) || other.id == id)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,imageUrl,prompt,happyPlace,description,name,createdAt,id); + +@override +String toString() { + return 'RecentLatteImage(imageUrl: $imageUrl, prompt: $prompt, happyPlace: $happyPlace, description: $description, name: $name, createdAt: $createdAt, id: $id)'; +} + + +} + +/// @nodoc +abstract mixin class $RecentLatteImageCopyWith<$Res> { + factory $RecentLatteImageCopyWith(RecentLatteImage value, $Res Function(RecentLatteImage) _then) = _$RecentLatteImageCopyWithImpl; +@useResult +$Res call({ + String imageUrl, String prompt, String happyPlace, String description, String name, DateTime createdAt, String? id +}); + + + + +} +/// @nodoc +class _$RecentLatteImageCopyWithImpl<$Res> + implements $RecentLatteImageCopyWith<$Res> { + _$RecentLatteImageCopyWithImpl(this._self, this._then); + + final RecentLatteImage _self; + final $Res Function(RecentLatteImage) _then; + +/// Create a copy of RecentLatteImage +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? imageUrl = null,Object? prompt = null,Object? happyPlace = null,Object? description = null,Object? name = null,Object? createdAt = null,Object? id = freezed,}) { + return _then(_self.copyWith( +imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String,prompt: null == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String,happyPlace: null == happyPlace ? _self.happyPlace : happyPlace // ignore: cast_nullable_to_non_nullable +as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [RecentLatteImage]. +extension RecentLatteImagePatterns on RecentLatteImage { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _RecentLatteImage value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _RecentLatteImage() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _RecentLatteImage value) $default,){ +final _that = this; +switch (_that) { +case _RecentLatteImage(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _RecentLatteImage value)? $default,){ +final _that = this; +switch (_that) { +case _RecentLatteImage() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String imageUrl, String prompt, String happyPlace, String description, String name, DateTime createdAt, String? id)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _RecentLatteImage() when $default != null: +return $default(_that.imageUrl,_that.prompt,_that.happyPlace,_that.description,_that.name,_that.createdAt,_that.id);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String imageUrl, String prompt, String happyPlace, String description, String name, DateTime createdAt, String? id) $default,) {final _that = this; +switch (_that) { +case _RecentLatteImage(): +return $default(_that.imageUrl,_that.prompt,_that.happyPlace,_that.description,_that.name,_that.createdAt,_that.id);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String imageUrl, String prompt, String happyPlace, String description, String name, DateTime createdAt, String? id)? $default,) {final _that = this; +switch (_that) { +case _RecentLatteImage() when $default != null: +return $default(_that.imageUrl,_that.prompt,_that.happyPlace,_that.description,_that.name,_that.createdAt,_that.id);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _RecentLatteImage extends RecentLatteImage { + const _RecentLatteImage({required this.imageUrl, required this.prompt, required this.happyPlace, required this.description, required this.name, required this.createdAt, this.id}): super._(); + factory _RecentLatteImage.fromJson(Map json) => _$RecentLatteImageFromJson(json); + +/// The URL of the generated image. +@override final String imageUrl; +/// The upgraded prompt sent to Gemini. +@override final String prompt; +/// The happy place that was used to generate the image. +@override final String happyPlace; +/// A description of the generated image. +@override final String description; +/// The name of the user who created this image. +@override final String name; +/// The time the originating [LatteOrder] was submitted and this document +/// was created. +@override final DateTime createdAt; +/// The ID of the generated image. +@override final String? id; + +/// Create a copy of RecentLatteImage +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$RecentLatteImageCopyWith<_RecentLatteImage> get copyWith => __$RecentLatteImageCopyWithImpl<_RecentLatteImage>(this, _$identity); + +@override +Map toJson() { + return _$RecentLatteImageToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecentLatteImage&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.prompt, prompt) || other.prompt == prompt)&&(identical(other.happyPlace, happyPlace) || other.happyPlace == happyPlace)&&(identical(other.description, description) || other.description == description)&&(identical(other.name, name) || other.name == name)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.id, id) || other.id == id)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,imageUrl,prompt,happyPlace,description,name,createdAt,id); + +@override +String toString() { + return 'RecentLatteImage(imageUrl: $imageUrl, prompt: $prompt, happyPlace: $happyPlace, description: $description, name: $name, createdAt: $createdAt, id: $id)'; +} + + +} + +/// @nodoc +abstract mixin class _$RecentLatteImageCopyWith<$Res> implements $RecentLatteImageCopyWith<$Res> { + factory _$RecentLatteImageCopyWith(_RecentLatteImage value, $Res Function(_RecentLatteImage) _then) = __$RecentLatteImageCopyWithImpl; +@override @useResult +$Res call({ + String imageUrl, String prompt, String happyPlace, String description, String name, DateTime createdAt, String? id +}); + + + + +} +/// @nodoc +class __$RecentLatteImageCopyWithImpl<$Res> + implements _$RecentLatteImageCopyWith<$Res> { + __$RecentLatteImageCopyWithImpl(this._self, this._then); + + final _RecentLatteImage _self; + final $Res Function(_RecentLatteImage) _then; + +/// Create a copy of RecentLatteImage +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? imageUrl = null,Object? prompt = null,Object? happyPlace = null,Object? description = null,Object? name = null,Object? createdAt = null,Object? id = freezed,}) { + return _then(_RecentLatteImage( +imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String,prompt: null == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String,happyPlace: null == happyPlace ? _self.happyPlace : happyPlace // ignore: cast_nullable_to_non_nullable +as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-data/lib/src/models/latte_image.g.dart b/genlatte/genlatte-data/lib/src/models/latte_image.g.dart new file mode 100644 index 0000000..a952e63 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/latte_image.g.dart @@ -0,0 +1,129 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'latte_image.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_LatteImageBatch _$LatteImageBatchFromJson(Map json) => + _LatteImageBatch( + id: json['id'] as String, + orderId: json['orderId'] as String, + image0: _$JsonConverterFromJson, LatteImage>( + json['image0'], + const LatteImageConverter().fromJson, + ), + image1: _$JsonConverterFromJson, LatteImage>( + json['image1'], + const LatteImageConverter().fromJson, + ), + image2: _$JsonConverterFromJson, LatteImage>( + json['image2'], + const LatteImageConverter().fromJson, + ), + image3: _$JsonConverterFromJson, LatteImage>( + json['image3'], + const LatteImageConverter().fromJson, + ), + parent: + _$JsonConverterFromJson, LatteImageBatchParent>( + json['parent'], + const LatteImageBatchParentConverter().fromJson, + ), + ); + +Map _$LatteImageBatchToJson( + _LatteImageBatch instance, +) => { + 'id': instance.id, + 'orderId': instance.orderId, + 'image0': _$JsonConverterToJson, LatteImage>( + instance.image0, + const LatteImageConverter().toJson, + ), + 'image1': _$JsonConverterToJson, LatteImage>( + instance.image1, + const LatteImageConverter().toJson, + ), + 'image2': _$JsonConverterToJson, LatteImage>( + instance.image2, + const LatteImageConverter().toJson, + ), + 'image3': _$JsonConverterToJson, LatteImage>( + instance.image3, + const LatteImageConverter().toJson, + ), + 'parent': _$JsonConverterToJson, LatteImageBatchParent>( + instance.parent, + const LatteImageBatchParentConverter().toJson, + ), +}; + +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => value == null ? null : toJson(value); + +_LatteImageBatchParent _$LatteImageBatchParentFromJson( + Map json, +) => _LatteImageBatchParent( + id: json['id'] as String, + imageIndex: json['imageIndex'] as String, +); + +Map _$LatteImageBatchParentToJson( + _LatteImageBatchParent instance, +) => {'id': instance.id, 'imageIndex': instance.imageIndex}; + +_LatteImage _$LatteImageFromJson(Map json) => _LatteImage( + imageUrl: json['imageUrl'] as String, + prompt: json['prompt'] as String, + questions: (json['questions'] as List?) + ?.map( + (e) => const QuestionConverter().fromJson(e as Map), + ) + .toList(), + description: json['description'] as String, +); + +Map _$LatteImageToJson(_LatteImage instance) => + { + 'imageUrl': instance.imageUrl, + 'prompt': instance.prompt, + 'questions': instance.questions + ?.map(const QuestionConverter().toJson) + .toList(), + 'description': instance.description, + }; + +_RecentLatteImage _$RecentLatteImageFromJson(Map json) => + _RecentLatteImage( + imageUrl: json['imageUrl'] as String, + prompt: json['prompt'] as String, + happyPlace: json['happyPlace'] as String, + description: json['description'] as String, + name: json['name'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + id: json['id'] as String?, + ); + +Map _$RecentLatteImageToJson(_RecentLatteImage instance) => + { + 'imageUrl': instance.imageUrl, + 'prompt': instance.prompt, + 'happyPlace': instance.happyPlace, + 'description': instance.description, + 'name': instance.name, + 'createdAt': instance.createdAt.toIso8601String(), + 'id': instance.id, + }; diff --git a/genlatte/genlatte-data/lib/src/models/latte_options.dart b/genlatte/genlatte-data/lib/src/models/latte_options.dart new file mode 100644 index 0000000..1e43c80 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/latte_options.dart @@ -0,0 +1,68 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport 'package:genlatte_data/src/models/latte_order.dart'; +library; + +import 'package:data_layer/data_layer.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'latte_options.freezed.dart'; +part 'latte_options.g.dart'; + +/// Milk and Sweetener options for a [LatteOrder]. +@freezed +abstract class LatteOptions with _$LatteOptions { + /// Instantiates a [LatteOptions]. + const factory LatteOptions({ + required String id, + @LatteOptionConverter() required List values, + }) = _LatteOptions; + + const LatteOptions._(); + + /// Json deserializer for [LatteOptions]. + factory LatteOptions.fromJson(Map json) => + _$LatteOptionsFromJson(json); + + /// Data Layer bindings for [LatteOptions]. + static Bindings bindings = Bindings( + fromJson: LatteOptions.fromJson, + toJson: (obj) => obj.toJson(), + getId: (obj) => obj.id, + getDetailUrl: (id) => ApiUrl(path: 'latteOptions/$id'), + getListUrl: () => const ApiUrl(path: 'latteOptions'), + ); +} + +/// An entry within [LatteOptions]. +/// +/// For example, if the surrounding [LatteOptions] is for "milk", the values +/// could be "whole", "skim", "oat". +@Freezed() +abstract class LatteOption with _$LatteOption { + /// Instantiates a [LatteOption]. + const factory LatteOption({ + required String name, + @Default(true) bool isAvailable, + }) = _LatteOption; + + const LatteOption._(); + + /// Json deserializer for [LatteOption]. + factory LatteOption.fromJson(Map json) => + _$LatteOptionFromJson(json); +} + +/// JsonConverter for [LatteOption] used in [LatteOptions.values]. +class LatteOptionConverter extends JsonConverter { + /// Instantiates a [LatteOptionConverter]. + const LatteOptionConverter(); + + @override + LatteOption fromJson(Json json) => LatteOption.fromJson(json); + + @override + Json toJson(LatteOption obj) => obj.toJson(); +} diff --git a/genlatte/genlatte-data/lib/src/models/latte_options.freezed.dart b/genlatte/genlatte-data/lib/src/models/latte_options.freezed.dart new file mode 100644 index 0000000..62e47d3 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/latte_options.freezed.dart @@ -0,0 +1,556 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'latte_options.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$LatteOptions { + + String get id;@LatteOptionConverter() List get values; +/// Create a copy of LatteOptions +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LatteOptionsCopyWith get copyWith => _$LatteOptionsCopyWithImpl(this as LatteOptions, _$identity); + + /// Serializes this LatteOptions to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LatteOptions&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.values, values)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,const DeepCollectionEquality().hash(values)); + +@override +String toString() { + return 'LatteOptions(id: $id, values: $values)'; +} + + +} + +/// @nodoc +abstract mixin class $LatteOptionsCopyWith<$Res> { + factory $LatteOptionsCopyWith(LatteOptions value, $Res Function(LatteOptions) _then) = _$LatteOptionsCopyWithImpl; +@useResult +$Res call({ + String id,@LatteOptionConverter() List values +}); + + + + +} +/// @nodoc +class _$LatteOptionsCopyWithImpl<$Res> + implements $LatteOptionsCopyWith<$Res> { + _$LatteOptionsCopyWithImpl(this._self, this._then); + + final LatteOptions _self; + final $Res Function(LatteOptions) _then; + +/// Create a copy of LatteOptions +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? values = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,values: null == values ? _self.values : values // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [LatteOptions]. +extension LatteOptionsPatterns on LatteOptions { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LatteOptions value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LatteOptions() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LatteOptions value) $default,){ +final _that = this; +switch (_that) { +case _LatteOptions(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LatteOptions value)? $default,){ +final _that = this; +switch (_that) { +case _LatteOptions() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, @LatteOptionConverter() List values)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LatteOptions() when $default != null: +return $default(_that.id,_that.values);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, @LatteOptionConverter() List values) $default,) {final _that = this; +switch (_that) { +case _LatteOptions(): +return $default(_that.id,_that.values);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, @LatteOptionConverter() List values)? $default,) {final _that = this; +switch (_that) { +case _LatteOptions() when $default != null: +return $default(_that.id,_that.values);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _LatteOptions extends LatteOptions { + const _LatteOptions({required this.id, @LatteOptionConverter() required final List values}): _values = values,super._(); + factory _LatteOptions.fromJson(Map json) => _$LatteOptionsFromJson(json); + +@override final String id; + final List _values; +@override@LatteOptionConverter() List get values { + if (_values is EqualUnmodifiableListView) return _values; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_values); +} + + +/// Create a copy of LatteOptions +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LatteOptionsCopyWith<_LatteOptions> get copyWith => __$LatteOptionsCopyWithImpl<_LatteOptions>(this, _$identity); + +@override +Map toJson() { + return _$LatteOptionsToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LatteOptions&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._values, _values)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,const DeepCollectionEquality().hash(_values)); + +@override +String toString() { + return 'LatteOptions(id: $id, values: $values)'; +} + + +} + +/// @nodoc +abstract mixin class _$LatteOptionsCopyWith<$Res> implements $LatteOptionsCopyWith<$Res> { + factory _$LatteOptionsCopyWith(_LatteOptions value, $Res Function(_LatteOptions) _then) = __$LatteOptionsCopyWithImpl; +@override @useResult +$Res call({ + String id,@LatteOptionConverter() List values +}); + + + + +} +/// @nodoc +class __$LatteOptionsCopyWithImpl<$Res> + implements _$LatteOptionsCopyWith<$Res> { + __$LatteOptionsCopyWithImpl(this._self, this._then); + + final _LatteOptions _self; + final $Res Function(_LatteOptions) _then; + +/// Create a copy of LatteOptions +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? values = null,}) { + return _then(_LatteOptions( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,values: null == values ? _self._values : values // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + + +/// @nodoc +mixin _$LatteOption { + + String get name; bool get isAvailable; +/// Create a copy of LatteOption +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LatteOptionCopyWith get copyWith => _$LatteOptionCopyWithImpl(this as LatteOption, _$identity); + + /// Serializes this LatteOption to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LatteOption&&(identical(other.name, name) || other.name == name)&&(identical(other.isAvailable, isAvailable) || other.isAvailable == isAvailable)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,name,isAvailable); + +@override +String toString() { + return 'LatteOption(name: $name, isAvailable: $isAvailable)'; +} + + +} + +/// @nodoc +abstract mixin class $LatteOptionCopyWith<$Res> { + factory $LatteOptionCopyWith(LatteOption value, $Res Function(LatteOption) _then) = _$LatteOptionCopyWithImpl; +@useResult +$Res call({ + String name, bool isAvailable +}); + + + + +} +/// @nodoc +class _$LatteOptionCopyWithImpl<$Res> + implements $LatteOptionCopyWith<$Res> { + _$LatteOptionCopyWithImpl(this._self, this._then); + + final LatteOption _self; + final $Res Function(LatteOption) _then; + +/// Create a copy of LatteOption +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? isAvailable = null,}) { + return _then(_self.copyWith( +name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,isAvailable: null == isAvailable ? _self.isAvailable : isAvailable // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [LatteOption]. +extension LatteOptionPatterns on LatteOption { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LatteOption value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LatteOption() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LatteOption value) $default,){ +final _that = this; +switch (_that) { +case _LatteOption(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LatteOption value)? $default,){ +final _that = this; +switch (_that) { +case _LatteOption() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String name, bool isAvailable)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LatteOption() when $default != null: +return $default(_that.name,_that.isAvailable);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String name, bool isAvailable) $default,) {final _that = this; +switch (_that) { +case _LatteOption(): +return $default(_that.name,_that.isAvailable);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String name, bool isAvailable)? $default,) {final _that = this; +switch (_that) { +case _LatteOption() when $default != null: +return $default(_that.name,_that.isAvailable);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _LatteOption extends LatteOption { + const _LatteOption({required this.name, this.isAvailable = true}): super._(); + factory _LatteOption.fromJson(Map json) => _$LatteOptionFromJson(json); + +@override final String name; +@override@JsonKey() final bool isAvailable; + +/// Create a copy of LatteOption +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LatteOptionCopyWith<_LatteOption> get copyWith => __$LatteOptionCopyWithImpl<_LatteOption>(this, _$identity); + +@override +Map toJson() { + return _$LatteOptionToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LatteOption&&(identical(other.name, name) || other.name == name)&&(identical(other.isAvailable, isAvailable) || other.isAvailable == isAvailable)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,name,isAvailable); + +@override +String toString() { + return 'LatteOption(name: $name, isAvailable: $isAvailable)'; +} + + +} + +/// @nodoc +abstract mixin class _$LatteOptionCopyWith<$Res> implements $LatteOptionCopyWith<$Res> { + factory _$LatteOptionCopyWith(_LatteOption value, $Res Function(_LatteOption) _then) = __$LatteOptionCopyWithImpl; +@override @useResult +$Res call({ + String name, bool isAvailable +}); + + + + +} +/// @nodoc +class __$LatteOptionCopyWithImpl<$Res> + implements _$LatteOptionCopyWith<$Res> { + __$LatteOptionCopyWithImpl(this._self, this._then); + + final _LatteOption _self; + final $Res Function(_LatteOption) _then; + +/// Create a copy of LatteOption +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? isAvailable = null,}) { + return _then(_LatteOption( +name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,isAvailable: null == isAvailable ? _self.isAvailable : isAvailable // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-data/lib/src/models/latte_options.g.dart b/genlatte/genlatte-data/lib/src/models/latte_options.g.dart new file mode 100644 index 0000000..f5237b8 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/latte_options.g.dart @@ -0,0 +1,41 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'latte_options.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_LatteOptions _$LatteOptionsFromJson(Map json) => + _LatteOptions( + id: json['id'] as String, + values: (json['values'] as List) + .map( + (e) => const LatteOptionConverter().fromJson( + e as Map, + ), + ) + .toList(), + ); + +Map _$LatteOptionsToJson( + _LatteOptions instance, +) => { + 'id': instance.id, + 'values': instance.values.map(const LatteOptionConverter().toJson).toList(), +}; + +_LatteOption _$LatteOptionFromJson(Map json) => _LatteOption( + name: json['name'] as String, + isAvailable: json['isAvailable'] as bool? ?? true, +); + +Map _$LatteOptionToJson(_LatteOption instance) => + { + 'name': instance.name, + 'isAvailable': instance.isAvailable, + }; diff --git a/genlatte/genlatte-data/lib/src/models/latte_order.dart b/genlatte/genlatte-data/lib/src/models/latte_order.dart new file mode 100644 index 0000000..727d314 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/latte_order.dart @@ -0,0 +1,232 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:data_layer/data_layer.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'latte_order.freezed.dart'; +part 'latte_order.g.dart'; + +/// One person's coffee order. Only contains fields the user may freely update. +/// Once a person +@freezed +abstract class LatteOrder with _$LatteOrder { + /// Creates a new [LatteOrder]. + const factory LatteOrder({ + /// Firestore-generated document Id. Not a user-friendly identifier. + String? id, + + /// The name of the person ordering. + String? name, + + /// The coffee's milk type. + /// + /// Null value for milk is not possible for a valid order, but is the + /// default order because the user must select a milk (as opposed to having + /// a default milk that they might overlook changing). + String? milk, + + /// The coffee's sweetener type, if any. + /// + /// Null value for sweetener IS possible because no sweetener is allowed. + String? sweetener, + + /// The prompt for the user's generated image. + String? happyPlace, + }) = _LatteOrder; + + const LatteOrder._(); + + /// Json deserializer for [LatteOrder]. + factory LatteOrder.fromJson(Map json) => + _$LatteOrderFromJson(json); + + /// Data Layer bindings for [LatteOrder]. + static Bindings bindings = Bindings( + fromJson: LatteOrder.fromJson, + toJson: (obj) => obj.toJson(), + getId: (obj) => obj.id, + getDetailUrl: (id) => ApiUrl(path: 'latteOrders/$id'), + getListUrl: () => const ApiUrl(path: 'latteOrders'), + ); +} + +/// Server-controlled information about a person's order which is not directly +/// editable by the user. Automated processes advance these values as the order +/// progresses through the pipeline. +/// +/// Security rules prevent this document from being written to by the client. +@freezed +abstract class LatteOrderMetadata with _$LatteOrderMetadata { + /// Creates a new [LatteOrderMetadata]. + const factory LatteOrderMetadata({ + /// Firestore-generated document Id. Not a user-friendly identifier. + /// This value is guaranteed to be the same as the corresponding + /// [LatteOrder.id]. + String? id, + + /// Sequential order number, set by the server and visible in the queue. + int? orderNumber, + + /// Gemini sets this to false or true upon moderation. Baristas optionally + /// set this to true when advancing an order's status to + /// [LatteOrderStatus.validated]. Additionally, a value of `true` is + /// required for this user's name to be shown on the big boards. + bool? isNameApproved, + + /// Gemini sets this to false or true upon moderation. Image generation is + /// gated by Gemini setting this value to `true`. Later, baristas may + /// override this to `false` when advancing an order's status to + /// [LatteOrderStatus.validated]. If a barista does this, the user will + /// receive a fallback image on their latte. + bool? isHappyPlaceApproved, + + /// The reason Gemini rejected the user's happy place prompt. If the image + /// was moderated by the barista, then this value should say + /// "barista_moderation". + String? happyPlaceModerationReason, + + /// Set to true once a barista provides final manual approval of the image. + /// This value must be true for an [LatteOrder] to advance to + /// [LatteOrderStatus.inProgress]. + bool? isImageApproved, + + /// The active image batch. + String? imageBatchId, + + /// Set once the user has committed to an image. + String? imageUrl, + + /// The reason an order was sent back from [LatteOrderStatus.submitted] to + /// [LatteOrderStatus.configuring] instead of being accepted and advanced to + /// [LatteOrderStatus.validated]. + // LatteOrderValidationError? validationError, + + /// {@macro LatteOrderStatus} + @Default(LatteOrderStatus.configuring) LatteOrderStatus status, + + /// Set to non-null once a barista claims the order. + String? baristaId, + + /// Set once the order status reaches [LatteOrderStatus.submitted]. + DateTime? orderSubmittedTime, + + /// Set once the order status reaches [LatteOrderStatus.completed]. + DateTime? completionTime, + }) = _LatteOrderMetadata; + + const LatteOrderMetadata._(); + + /// Json deserializer for [LatteOrderMetadata]. + factory LatteOrderMetadata.fromJson(Map json) => + _$LatteOrderMetadataFromJson(json); + + /// Data Layer bindings for [LatteOrderMetadata]. + static Bindings bindings = Bindings( + fromJson: LatteOrderMetadata.fromJson, + toJson: (obj) => obj.toJson(), + getId: (obj) => obj.id, + getDetailUrl: (id) => ApiUrl(path: 'latteOrderMetadata/$id'), + getListUrl: () => const ApiUrl(path: 'latteOrderMetadata'), + ); + + /// Validates that the order and metadata are valid for the given status. + /// + /// The idea is that the Order and its metadata have just been updated, + /// presumably including a new `metadata.status` value, and we want to make + /// sure that everything is as expected before saving this new value. A + /// non-null return value from this method should cause the calling code to + /// abort any save operations and return to the previous [LatteOrder] object. + LatteOrderValidationError? validate(LatteOrder order) { + switch (status) { + case .configuring: + return null; + case .submitted: + if (order.milk == null) { + return .missingMilk; + } + if (order.sweetener == null) { + return .missingSweetener; + } + if (imageUrl == null) { + return .imageNotSet; + } + return null; + case .validated || .inProgress || .completed || .archived: + return null; + } + } +} + +/// Container for a [LatteOrder] and [LatteOrderMetadata]. This is not a +/// server-side construct and only exists on the client to pair a related order +/// and its metadata. +@freezed +abstract class Latte with _$Latte { + /// Creates a new [Latte]. + const factory Latte({ + required LatteOrder order, + required LatteOrderMetadata metadata, + }) = _Latte; + + const Latte._(); + + /// Json deserializer for [Latte]. + factory Latte.fromJson(Map json) => _$LatteFromJson(json); +} + +/// {@template LatteOrderStatus} +/// The delivery progress of an [LatteOrder]. +/// {@endtemplate} +enum LatteOrderStatus { + /// The order is in the database but not yet verified and submitted. These + /// orders are not visible to baristas. + configuring, + + /// Order now successfully submitted, but not yet validated. + submitted, + + /// Order has been accepted by the server but is not yet picked up by a + /// barista. + validated, + + /// A barista has started working on the order. + inProgress, + + /// The order has been completed and is ready for pickup. + completed, + + /// The order has been completed a long time ago and we should not + /// be downloading it from the repository in most cases. + archived, +} + +/// Ways an [LatteOrder] can fail validation. +enum LatteOrderValidationError { + /// The order is missing a milk type but attempted to enter status + /// [LatteOrderStatus.submitted]. + missingMilk, + + /// The latte order is missing a sweetener but attempted to enter status + /// [LatteOrderStatus.submitted]. + missingSweetener, + + /// The order image has not been set but attempted to enter status + /// [LatteOrderStatus.submitted]. + imageNotSet, +} + +/// Operations for lists of [LatteOrderMetadata] objects. +extension LatteOrderMetadataList on List { + /// Converts the list of orders into a map keyed by order id. + Map toIdsMap() { + return fold>( + {}, + (map, order) { + map[order.id!] = order; + return map; + }, + ); + } +} diff --git a/genlatte/genlatte-data/lib/src/models/latte_order.freezed.dart b/genlatte/genlatte-data/lib/src/models/latte_order.freezed.dart new file mode 100644 index 0000000..b6db760 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/latte_order.freezed.dart @@ -0,0 +1,971 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'latte_order.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$LatteOrder { + +/// Firestore-generated document Id. Not a user-friendly identifier. + String? get id;/// The name of the person ordering. + String? get name;/// The coffee's milk type. +/// +/// Null value for milk is not possible for a valid order, but is the +/// default order because the user must select a milk (as opposed to having +/// a default milk that they might overlook changing). + String? get milk;/// The coffee's sweetener type, if any. +/// +/// Null value for sweetener IS possible because no sweetener is allowed. + String? get sweetener;/// The prompt for the user's generated image. + String? get happyPlace; +/// Create a copy of LatteOrder +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LatteOrderCopyWith get copyWith => _$LatteOrderCopyWithImpl(this as LatteOrder, _$identity); + + /// Serializes this LatteOrder to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LatteOrder&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.milk, milk) || other.milk == milk)&&(identical(other.sweetener, sweetener) || other.sweetener == sweetener)&&(identical(other.happyPlace, happyPlace) || other.happyPlace == happyPlace)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,name,milk,sweetener,happyPlace); + +@override +String toString() { + return 'LatteOrder(id: $id, name: $name, milk: $milk, sweetener: $sweetener, happyPlace: $happyPlace)'; +} + + +} + +/// @nodoc +abstract mixin class $LatteOrderCopyWith<$Res> { + factory $LatteOrderCopyWith(LatteOrder value, $Res Function(LatteOrder) _then) = _$LatteOrderCopyWithImpl; +@useResult +$Res call({ + String? id, String? name, String? milk, String? sweetener, String? happyPlace +}); + + + + +} +/// @nodoc +class _$LatteOrderCopyWithImpl<$Res> + implements $LatteOrderCopyWith<$Res> { + _$LatteOrderCopyWithImpl(this._self, this._then); + + final LatteOrder _self; + final $Res Function(LatteOrder) _then; + +/// Create a copy of LatteOrder +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? name = freezed,Object? milk = freezed,Object? sweetener = freezed,Object? happyPlace = freezed,}) { + return _then(_self.copyWith( +id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String?,milk: freezed == milk ? _self.milk : milk // ignore: cast_nullable_to_non_nullable +as String?,sweetener: freezed == sweetener ? _self.sweetener : sweetener // ignore: cast_nullable_to_non_nullable +as String?,happyPlace: freezed == happyPlace ? _self.happyPlace : happyPlace // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [LatteOrder]. +extension LatteOrderPatterns on LatteOrder { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LatteOrder value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LatteOrder() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LatteOrder value) $default,){ +final _that = this; +switch (_that) { +case _LatteOrder(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LatteOrder value)? $default,){ +final _that = this; +switch (_that) { +case _LatteOrder() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? id, String? name, String? milk, String? sweetener, String? happyPlace)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LatteOrder() when $default != null: +return $default(_that.id,_that.name,_that.milk,_that.sweetener,_that.happyPlace);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? id, String? name, String? milk, String? sweetener, String? happyPlace) $default,) {final _that = this; +switch (_that) { +case _LatteOrder(): +return $default(_that.id,_that.name,_that.milk,_that.sweetener,_that.happyPlace);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? id, String? name, String? milk, String? sweetener, String? happyPlace)? $default,) {final _that = this; +switch (_that) { +case _LatteOrder() when $default != null: +return $default(_that.id,_that.name,_that.milk,_that.sweetener,_that.happyPlace);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _LatteOrder extends LatteOrder { + const _LatteOrder({this.id, this.name, this.milk, this.sweetener, this.happyPlace}): super._(); + factory _LatteOrder.fromJson(Map json) => _$LatteOrderFromJson(json); + +/// Firestore-generated document Id. Not a user-friendly identifier. +@override final String? id; +/// The name of the person ordering. +@override final String? name; +/// The coffee's milk type. +/// +/// Null value for milk is not possible for a valid order, but is the +/// default order because the user must select a milk (as opposed to having +/// a default milk that they might overlook changing). +@override final String? milk; +/// The coffee's sweetener type, if any. +/// +/// Null value for sweetener IS possible because no sweetener is allowed. +@override final String? sweetener; +/// The prompt for the user's generated image. +@override final String? happyPlace; + +/// Create a copy of LatteOrder +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LatteOrderCopyWith<_LatteOrder> get copyWith => __$LatteOrderCopyWithImpl<_LatteOrder>(this, _$identity); + +@override +Map toJson() { + return _$LatteOrderToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LatteOrder&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.milk, milk) || other.milk == milk)&&(identical(other.sweetener, sweetener) || other.sweetener == sweetener)&&(identical(other.happyPlace, happyPlace) || other.happyPlace == happyPlace)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,name,milk,sweetener,happyPlace); + +@override +String toString() { + return 'LatteOrder(id: $id, name: $name, milk: $milk, sweetener: $sweetener, happyPlace: $happyPlace)'; +} + + +} + +/// @nodoc +abstract mixin class _$LatteOrderCopyWith<$Res> implements $LatteOrderCopyWith<$Res> { + factory _$LatteOrderCopyWith(_LatteOrder value, $Res Function(_LatteOrder) _then) = __$LatteOrderCopyWithImpl; +@override @useResult +$Res call({ + String? id, String? name, String? milk, String? sweetener, String? happyPlace +}); + + + + +} +/// @nodoc +class __$LatteOrderCopyWithImpl<$Res> + implements _$LatteOrderCopyWith<$Res> { + __$LatteOrderCopyWithImpl(this._self, this._then); + + final _LatteOrder _self; + final $Res Function(_LatteOrder) _then; + +/// Create a copy of LatteOrder +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? name = freezed,Object? milk = freezed,Object? sweetener = freezed,Object? happyPlace = freezed,}) { + return _then(_LatteOrder( +id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String?,milk: freezed == milk ? _self.milk : milk // ignore: cast_nullable_to_non_nullable +as String?,sweetener: freezed == sweetener ? _self.sweetener : sweetener // ignore: cast_nullable_to_non_nullable +as String?,happyPlace: freezed == happyPlace ? _self.happyPlace : happyPlace // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + + +/// @nodoc +mixin _$LatteOrderMetadata { + +/// Firestore-generated document Id. Not a user-friendly identifier. +/// This value is guaranteed to be the same as the corresponding +/// [LatteOrder.id]. + String? get id;/// Sequential order number, set by the server and visible in the queue. + int? get orderNumber;/// Gemini sets this to false or true upon moderation. Baristas optionally +/// set this to true when advancing an order's status to +/// [LatteOrderStatus.validated]. Additionally, a value of `true` is +/// required for this user's name to be shown on the big boards. + bool? get isNameApproved;/// Gemini sets this to false or true upon moderation. Image generation is +/// gated by Gemini setting this value to `true`. Later, baristas may +/// override this to `false` when advancing an order's status to +/// [LatteOrderStatus.validated]. If a barista does this, the user will +/// receive a fallback image on their latte. + bool? get isHappyPlaceApproved;/// The reason Gemini rejected the user's happy place prompt. If the image +/// was moderated by the barista, then this value should say +/// "barista_moderation". + String? get happyPlaceModerationReason;/// Set to true once a barista provides final manual approval of the image. +/// This value must be true for an [LatteOrder] to advance to +/// [LatteOrderStatus.inProgress]. + bool? get isImageApproved;/// The active image batch. + String? get imageBatchId;/// Set once the user has committed to an image. + String? get imageUrl;/// The reason an order was sent back from [LatteOrderStatus.submitted] to +/// [LatteOrderStatus.configuring] instead of being accepted and advanced to +/// [LatteOrderStatus.validated]. +// LatteOrderValidationError? validationError, +/// {@macro LatteOrderStatus} + LatteOrderStatus get status;/// Set to non-null once a barista claims the order. + String? get baristaId;/// Set once the order status reaches [LatteOrderStatus.submitted]. + DateTime? get orderSubmittedTime;/// Set once the order status reaches [LatteOrderStatus.completed]. + DateTime? get completionTime; +/// Create a copy of LatteOrderMetadata +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LatteOrderMetadataCopyWith get copyWith => _$LatteOrderMetadataCopyWithImpl(this as LatteOrderMetadata, _$identity); + + /// Serializes this LatteOrderMetadata to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LatteOrderMetadata&&(identical(other.id, id) || other.id == id)&&(identical(other.orderNumber, orderNumber) || other.orderNumber == orderNumber)&&(identical(other.isNameApproved, isNameApproved) || other.isNameApproved == isNameApproved)&&(identical(other.isHappyPlaceApproved, isHappyPlaceApproved) || other.isHappyPlaceApproved == isHappyPlaceApproved)&&(identical(other.happyPlaceModerationReason, happyPlaceModerationReason) || other.happyPlaceModerationReason == happyPlaceModerationReason)&&(identical(other.isImageApproved, isImageApproved) || other.isImageApproved == isImageApproved)&&(identical(other.imageBatchId, imageBatchId) || other.imageBatchId == imageBatchId)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.status, status) || other.status == status)&&(identical(other.baristaId, baristaId) || other.baristaId == baristaId)&&(identical(other.orderSubmittedTime, orderSubmittedTime) || other.orderSubmittedTime == orderSubmittedTime)&&(identical(other.completionTime, completionTime) || other.completionTime == completionTime)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,orderNumber,isNameApproved,isHappyPlaceApproved,happyPlaceModerationReason,isImageApproved,imageBatchId,imageUrl,status,baristaId,orderSubmittedTime,completionTime); + +@override +String toString() { + return 'LatteOrderMetadata(id: $id, orderNumber: $orderNumber, isNameApproved: $isNameApproved, isHappyPlaceApproved: $isHappyPlaceApproved, happyPlaceModerationReason: $happyPlaceModerationReason, isImageApproved: $isImageApproved, imageBatchId: $imageBatchId, imageUrl: $imageUrl, status: $status, baristaId: $baristaId, orderSubmittedTime: $orderSubmittedTime, completionTime: $completionTime)'; +} + + +} + +/// @nodoc +abstract mixin class $LatteOrderMetadataCopyWith<$Res> { + factory $LatteOrderMetadataCopyWith(LatteOrderMetadata value, $Res Function(LatteOrderMetadata) _then) = _$LatteOrderMetadataCopyWithImpl; +@useResult +$Res call({ + String? id, int? orderNumber, bool? isNameApproved, bool? isHappyPlaceApproved, String? happyPlaceModerationReason, bool? isImageApproved, String? imageBatchId, String? imageUrl, LatteOrderStatus status, String? baristaId, DateTime? orderSubmittedTime, DateTime? completionTime +}); + + + + +} +/// @nodoc +class _$LatteOrderMetadataCopyWithImpl<$Res> + implements $LatteOrderMetadataCopyWith<$Res> { + _$LatteOrderMetadataCopyWithImpl(this._self, this._then); + + final LatteOrderMetadata _self; + final $Res Function(LatteOrderMetadata) _then; + +/// Create a copy of LatteOrderMetadata +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? orderNumber = freezed,Object? isNameApproved = freezed,Object? isHappyPlaceApproved = freezed,Object? happyPlaceModerationReason = freezed,Object? isImageApproved = freezed,Object? imageBatchId = freezed,Object? imageUrl = freezed,Object? status = null,Object? baristaId = freezed,Object? orderSubmittedTime = freezed,Object? completionTime = freezed,}) { + return _then(_self.copyWith( +id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String?,orderNumber: freezed == orderNumber ? _self.orderNumber : orderNumber // ignore: cast_nullable_to_non_nullable +as int?,isNameApproved: freezed == isNameApproved ? _self.isNameApproved : isNameApproved // ignore: cast_nullable_to_non_nullable +as bool?,isHappyPlaceApproved: freezed == isHappyPlaceApproved ? _self.isHappyPlaceApproved : isHappyPlaceApproved // ignore: cast_nullable_to_non_nullable +as bool?,happyPlaceModerationReason: freezed == happyPlaceModerationReason ? _self.happyPlaceModerationReason : happyPlaceModerationReason // ignore: cast_nullable_to_non_nullable +as String?,isImageApproved: freezed == isImageApproved ? _self.isImageApproved : isImageApproved // ignore: cast_nullable_to_non_nullable +as bool?,imageBatchId: freezed == imageBatchId ? _self.imageBatchId : imageBatchId // ignore: cast_nullable_to_non_nullable +as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as LatteOrderStatus,baristaId: freezed == baristaId ? _self.baristaId : baristaId // ignore: cast_nullable_to_non_nullable +as String?,orderSubmittedTime: freezed == orderSubmittedTime ? _self.orderSubmittedTime : orderSubmittedTime // ignore: cast_nullable_to_non_nullable +as DateTime?,completionTime: freezed == completionTime ? _self.completionTime : completionTime // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [LatteOrderMetadata]. +extension LatteOrderMetadataPatterns on LatteOrderMetadata { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LatteOrderMetadata value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LatteOrderMetadata() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LatteOrderMetadata value) $default,){ +final _that = this; +switch (_that) { +case _LatteOrderMetadata(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LatteOrderMetadata value)? $default,){ +final _that = this; +switch (_that) { +case _LatteOrderMetadata() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? id, int? orderNumber, bool? isNameApproved, bool? isHappyPlaceApproved, String? happyPlaceModerationReason, bool? isImageApproved, String? imageBatchId, String? imageUrl, LatteOrderStatus status, String? baristaId, DateTime? orderSubmittedTime, DateTime? completionTime)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LatteOrderMetadata() when $default != null: +return $default(_that.id,_that.orderNumber,_that.isNameApproved,_that.isHappyPlaceApproved,_that.happyPlaceModerationReason,_that.isImageApproved,_that.imageBatchId,_that.imageUrl,_that.status,_that.baristaId,_that.orderSubmittedTime,_that.completionTime);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? id, int? orderNumber, bool? isNameApproved, bool? isHappyPlaceApproved, String? happyPlaceModerationReason, bool? isImageApproved, String? imageBatchId, String? imageUrl, LatteOrderStatus status, String? baristaId, DateTime? orderSubmittedTime, DateTime? completionTime) $default,) {final _that = this; +switch (_that) { +case _LatteOrderMetadata(): +return $default(_that.id,_that.orderNumber,_that.isNameApproved,_that.isHappyPlaceApproved,_that.happyPlaceModerationReason,_that.isImageApproved,_that.imageBatchId,_that.imageUrl,_that.status,_that.baristaId,_that.orderSubmittedTime,_that.completionTime);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? id, int? orderNumber, bool? isNameApproved, bool? isHappyPlaceApproved, String? happyPlaceModerationReason, bool? isImageApproved, String? imageBatchId, String? imageUrl, LatteOrderStatus status, String? baristaId, DateTime? orderSubmittedTime, DateTime? completionTime)? $default,) {final _that = this; +switch (_that) { +case _LatteOrderMetadata() when $default != null: +return $default(_that.id,_that.orderNumber,_that.isNameApproved,_that.isHappyPlaceApproved,_that.happyPlaceModerationReason,_that.isImageApproved,_that.imageBatchId,_that.imageUrl,_that.status,_that.baristaId,_that.orderSubmittedTime,_that.completionTime);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _LatteOrderMetadata extends LatteOrderMetadata { + const _LatteOrderMetadata({this.id, this.orderNumber, this.isNameApproved, this.isHappyPlaceApproved, this.happyPlaceModerationReason, this.isImageApproved, this.imageBatchId, this.imageUrl, this.status = LatteOrderStatus.configuring, this.baristaId, this.orderSubmittedTime, this.completionTime}): super._(); + factory _LatteOrderMetadata.fromJson(Map json) => _$LatteOrderMetadataFromJson(json); + +/// Firestore-generated document Id. Not a user-friendly identifier. +/// This value is guaranteed to be the same as the corresponding +/// [LatteOrder.id]. +@override final String? id; +/// Sequential order number, set by the server and visible in the queue. +@override final int? orderNumber; +/// Gemini sets this to false or true upon moderation. Baristas optionally +/// set this to true when advancing an order's status to +/// [LatteOrderStatus.validated]. Additionally, a value of `true` is +/// required for this user's name to be shown on the big boards. +@override final bool? isNameApproved; +/// Gemini sets this to false or true upon moderation. Image generation is +/// gated by Gemini setting this value to `true`. Later, baristas may +/// override this to `false` when advancing an order's status to +/// [LatteOrderStatus.validated]. If a barista does this, the user will +/// receive a fallback image on their latte. +@override final bool? isHappyPlaceApproved; +/// The reason Gemini rejected the user's happy place prompt. If the image +/// was moderated by the barista, then this value should say +/// "barista_moderation". +@override final String? happyPlaceModerationReason; +/// Set to true once a barista provides final manual approval of the image. +/// This value must be true for an [LatteOrder] to advance to +/// [LatteOrderStatus.inProgress]. +@override final bool? isImageApproved; +/// The active image batch. +@override final String? imageBatchId; +/// Set once the user has committed to an image. +@override final String? imageUrl; +/// The reason an order was sent back from [LatteOrderStatus.submitted] to +/// [LatteOrderStatus.configuring] instead of being accepted and advanced to +/// [LatteOrderStatus.validated]. +// LatteOrderValidationError? validationError, +/// {@macro LatteOrderStatus} +@override@JsonKey() final LatteOrderStatus status; +/// Set to non-null once a barista claims the order. +@override final String? baristaId; +/// Set once the order status reaches [LatteOrderStatus.submitted]. +@override final DateTime? orderSubmittedTime; +/// Set once the order status reaches [LatteOrderStatus.completed]. +@override final DateTime? completionTime; + +/// Create a copy of LatteOrderMetadata +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LatteOrderMetadataCopyWith<_LatteOrderMetadata> get copyWith => __$LatteOrderMetadataCopyWithImpl<_LatteOrderMetadata>(this, _$identity); + +@override +Map toJson() { + return _$LatteOrderMetadataToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LatteOrderMetadata&&(identical(other.id, id) || other.id == id)&&(identical(other.orderNumber, orderNumber) || other.orderNumber == orderNumber)&&(identical(other.isNameApproved, isNameApproved) || other.isNameApproved == isNameApproved)&&(identical(other.isHappyPlaceApproved, isHappyPlaceApproved) || other.isHappyPlaceApproved == isHappyPlaceApproved)&&(identical(other.happyPlaceModerationReason, happyPlaceModerationReason) || other.happyPlaceModerationReason == happyPlaceModerationReason)&&(identical(other.isImageApproved, isImageApproved) || other.isImageApproved == isImageApproved)&&(identical(other.imageBatchId, imageBatchId) || other.imageBatchId == imageBatchId)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.status, status) || other.status == status)&&(identical(other.baristaId, baristaId) || other.baristaId == baristaId)&&(identical(other.orderSubmittedTime, orderSubmittedTime) || other.orderSubmittedTime == orderSubmittedTime)&&(identical(other.completionTime, completionTime) || other.completionTime == completionTime)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,orderNumber,isNameApproved,isHappyPlaceApproved,happyPlaceModerationReason,isImageApproved,imageBatchId,imageUrl,status,baristaId,orderSubmittedTime,completionTime); + +@override +String toString() { + return 'LatteOrderMetadata(id: $id, orderNumber: $orderNumber, isNameApproved: $isNameApproved, isHappyPlaceApproved: $isHappyPlaceApproved, happyPlaceModerationReason: $happyPlaceModerationReason, isImageApproved: $isImageApproved, imageBatchId: $imageBatchId, imageUrl: $imageUrl, status: $status, baristaId: $baristaId, orderSubmittedTime: $orderSubmittedTime, completionTime: $completionTime)'; +} + + +} + +/// @nodoc +abstract mixin class _$LatteOrderMetadataCopyWith<$Res> implements $LatteOrderMetadataCopyWith<$Res> { + factory _$LatteOrderMetadataCopyWith(_LatteOrderMetadata value, $Res Function(_LatteOrderMetadata) _then) = __$LatteOrderMetadataCopyWithImpl; +@override @useResult +$Res call({ + String? id, int? orderNumber, bool? isNameApproved, bool? isHappyPlaceApproved, String? happyPlaceModerationReason, bool? isImageApproved, String? imageBatchId, String? imageUrl, LatteOrderStatus status, String? baristaId, DateTime? orderSubmittedTime, DateTime? completionTime +}); + + + + +} +/// @nodoc +class __$LatteOrderMetadataCopyWithImpl<$Res> + implements _$LatteOrderMetadataCopyWith<$Res> { + __$LatteOrderMetadataCopyWithImpl(this._self, this._then); + + final _LatteOrderMetadata _self; + final $Res Function(_LatteOrderMetadata) _then; + +/// Create a copy of LatteOrderMetadata +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? orderNumber = freezed,Object? isNameApproved = freezed,Object? isHappyPlaceApproved = freezed,Object? happyPlaceModerationReason = freezed,Object? isImageApproved = freezed,Object? imageBatchId = freezed,Object? imageUrl = freezed,Object? status = null,Object? baristaId = freezed,Object? orderSubmittedTime = freezed,Object? completionTime = freezed,}) { + return _then(_LatteOrderMetadata( +id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String?,orderNumber: freezed == orderNumber ? _self.orderNumber : orderNumber // ignore: cast_nullable_to_non_nullable +as int?,isNameApproved: freezed == isNameApproved ? _self.isNameApproved : isNameApproved // ignore: cast_nullable_to_non_nullable +as bool?,isHappyPlaceApproved: freezed == isHappyPlaceApproved ? _self.isHappyPlaceApproved : isHappyPlaceApproved // ignore: cast_nullable_to_non_nullable +as bool?,happyPlaceModerationReason: freezed == happyPlaceModerationReason ? _self.happyPlaceModerationReason : happyPlaceModerationReason // ignore: cast_nullable_to_non_nullable +as String?,isImageApproved: freezed == isImageApproved ? _self.isImageApproved : isImageApproved // ignore: cast_nullable_to_non_nullable +as bool?,imageBatchId: freezed == imageBatchId ? _self.imageBatchId : imageBatchId // ignore: cast_nullable_to_non_nullable +as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as LatteOrderStatus,baristaId: freezed == baristaId ? _self.baristaId : baristaId // ignore: cast_nullable_to_non_nullable +as String?,orderSubmittedTime: freezed == orderSubmittedTime ? _self.orderSubmittedTime : orderSubmittedTime // ignore: cast_nullable_to_non_nullable +as DateTime?,completionTime: freezed == completionTime ? _self.completionTime : completionTime // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + + +} + + +/// @nodoc +mixin _$Latte { + + LatteOrder get order; LatteOrderMetadata get metadata; +/// Create a copy of Latte +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LatteCopyWith get copyWith => _$LatteCopyWithImpl(this as Latte, _$identity); + + /// Serializes this Latte to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Latte&&(identical(other.order, order) || other.order == order)&&(identical(other.metadata, metadata) || other.metadata == metadata)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,order,metadata); + +@override +String toString() { + return 'Latte(order: $order, metadata: $metadata)'; +} + + +} + +/// @nodoc +abstract mixin class $LatteCopyWith<$Res> { + factory $LatteCopyWith(Latte value, $Res Function(Latte) _then) = _$LatteCopyWithImpl; +@useResult +$Res call({ + LatteOrder order, LatteOrderMetadata metadata +}); + + +$LatteOrderCopyWith<$Res> get order;$LatteOrderMetadataCopyWith<$Res> get metadata; + +} +/// @nodoc +class _$LatteCopyWithImpl<$Res> + implements $LatteCopyWith<$Res> { + _$LatteCopyWithImpl(this._self, this._then); + + final Latte _self; + final $Res Function(Latte) _then; + +/// Create a copy of Latte +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? order = null,Object? metadata = null,}) { + return _then(_self.copyWith( +order: null == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as LatteOrder,metadata: null == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable +as LatteOrderMetadata, + )); +} +/// Create a copy of Latte +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderCopyWith<$Res> get order { + + return $LatteOrderCopyWith<$Res>(_self.order, (value) { + return _then(_self.copyWith(order: value)); + }); +}/// Create a copy of Latte +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderMetadataCopyWith<$Res> get metadata { + + return $LatteOrderMetadataCopyWith<$Res>(_self.metadata, (value) { + return _then(_self.copyWith(metadata: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [Latte]. +extension LattePatterns on Latte { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Latte value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Latte() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Latte value) $default,){ +final _that = this; +switch (_that) { +case _Latte(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Latte value)? $default,){ +final _that = this; +switch (_that) { +case _Latte() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( LatteOrder order, LatteOrderMetadata metadata)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Latte() when $default != null: +return $default(_that.order,_that.metadata);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( LatteOrder order, LatteOrderMetadata metadata) $default,) {final _that = this; +switch (_that) { +case _Latte(): +return $default(_that.order,_that.metadata);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( LatteOrder order, LatteOrderMetadata metadata)? $default,) {final _that = this; +switch (_that) { +case _Latte() when $default != null: +return $default(_that.order,_that.metadata);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _Latte extends Latte { + const _Latte({required this.order, required this.metadata}): super._(); + factory _Latte.fromJson(Map json) => _$LatteFromJson(json); + +@override final LatteOrder order; +@override final LatteOrderMetadata metadata; + +/// Create a copy of Latte +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LatteCopyWith<_Latte> get copyWith => __$LatteCopyWithImpl<_Latte>(this, _$identity); + +@override +Map toJson() { + return _$LatteToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Latte&&(identical(other.order, order) || other.order == order)&&(identical(other.metadata, metadata) || other.metadata == metadata)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,order,metadata); + +@override +String toString() { + return 'Latte(order: $order, metadata: $metadata)'; +} + + +} + +/// @nodoc +abstract mixin class _$LatteCopyWith<$Res> implements $LatteCopyWith<$Res> { + factory _$LatteCopyWith(_Latte value, $Res Function(_Latte) _then) = __$LatteCopyWithImpl; +@override @useResult +$Res call({ + LatteOrder order, LatteOrderMetadata metadata +}); + + +@override $LatteOrderCopyWith<$Res> get order;@override $LatteOrderMetadataCopyWith<$Res> get metadata; + +} +/// @nodoc +class __$LatteCopyWithImpl<$Res> + implements _$LatteCopyWith<$Res> { + __$LatteCopyWithImpl(this._self, this._then); + + final _Latte _self; + final $Res Function(_Latte) _then; + +/// Create a copy of Latte +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? order = null,Object? metadata = null,}) { + return _then(_Latte( +order: null == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as LatteOrder,metadata: null == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable +as LatteOrderMetadata, + )); +} + +/// Create a copy of Latte +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderCopyWith<$Res> get order { + + return $LatteOrderCopyWith<$Res>(_self.order, (value) { + return _then(_self.copyWith(order: value)); + }); +}/// Create a copy of Latte +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderMetadataCopyWith<$Res> get metadata { + + return $LatteOrderMetadataCopyWith<$Res>(_self.metadata, (value) { + return _then(_self.copyWith(metadata: value)); + }); +} +} + +// dart format on diff --git a/genlatte/genlatte-data/lib/src/models/latte_order.g.dart b/genlatte/genlatte-data/lib/src/models/latte_order.g.dart new file mode 100644 index 0000000..15a2b14 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/latte_order.g.dart @@ -0,0 +1,87 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'latte_order.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_LatteOrder _$LatteOrderFromJson(Map json) => _LatteOrder( + id: json['id'] as String?, + name: json['name'] as String?, + milk: json['milk'] as String?, + sweetener: json['sweetener'] as String?, + happyPlace: json['happyPlace'] as String?, +); + +Map _$LatteOrderToJson(_LatteOrder instance) => + { + 'id': instance.id, + 'name': instance.name, + 'milk': instance.milk, + 'sweetener': instance.sweetener, + 'happyPlace': instance.happyPlace, + }; + +_LatteOrderMetadata _$LatteOrderMetadataFromJson(Map json) => + _LatteOrderMetadata( + id: json['id'] as String?, + orderNumber: (json['orderNumber'] as num?)?.toInt(), + isNameApproved: json['isNameApproved'] as bool?, + isHappyPlaceApproved: json['isHappyPlaceApproved'] as bool?, + happyPlaceModerationReason: json['happyPlaceModerationReason'] as String?, + isImageApproved: json['isImageApproved'] as bool?, + imageBatchId: json['imageBatchId'] as String?, + imageUrl: json['imageUrl'] as String?, + status: + $enumDecodeNullable(_$LatteOrderStatusEnumMap, json['status']) ?? + LatteOrderStatus.configuring, + baristaId: json['baristaId'] as String?, + orderSubmittedTime: json['orderSubmittedTime'] == null + ? null + : DateTime.parse(json['orderSubmittedTime'] as String), + completionTime: json['completionTime'] == null + ? null + : DateTime.parse(json['completionTime'] as String), + ); + +Map _$LatteOrderMetadataToJson(_LatteOrderMetadata instance) => + { + 'id': instance.id, + 'orderNumber': instance.orderNumber, + 'isNameApproved': instance.isNameApproved, + 'isHappyPlaceApproved': instance.isHappyPlaceApproved, + 'happyPlaceModerationReason': instance.happyPlaceModerationReason, + 'isImageApproved': instance.isImageApproved, + 'imageBatchId': instance.imageBatchId, + 'imageUrl': instance.imageUrl, + 'status': _$LatteOrderStatusEnumMap[instance.status]!, + 'baristaId': instance.baristaId, + 'orderSubmittedTime': instance.orderSubmittedTime?.toIso8601String(), + 'completionTime': instance.completionTime?.toIso8601String(), + }; + +const _$LatteOrderStatusEnumMap = { + LatteOrderStatus.configuring: 'configuring', + LatteOrderStatus.submitted: 'submitted', + LatteOrderStatus.validated: 'validated', + LatteOrderStatus.inProgress: 'inProgress', + LatteOrderStatus.completed: 'completed', + LatteOrderStatus.archived: 'archived', +}; + +_Latte _$LatteFromJson(Map json) => _Latte( + order: LatteOrder.fromJson(json['order'] as Map), + metadata: LatteOrderMetadata.fromJson( + json['metadata'] as Map, + ), +); + +Map _$LatteToJson(_Latte instance) => { + 'order': instance.order, + 'metadata': instance.metadata, +}; diff --git a/genlatte/genlatte-data/lib/src/models/machine.dart b/genlatte/genlatte-data/lib/src/models/machine.dart new file mode 100644 index 0000000..b8e24eb --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/machine.dart @@ -0,0 +1,49 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:data_layer/data_layer.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'machine.freezed.dart'; +part 'machine.g.dart'; + +/// An available printer. +@freezed +abstract class Machine with _$Machine { + /// Instantiates a new [Machine]. + const factory Machine({ + required String id, + required String name, + @Default(true) bool isActive, + @Default(true) bool isBlackAndWhite, + }) = _Machine; + const Machine._(); + + /// Json factory for [Machine]. + factory Machine.fromJson(Map json) => + _$MachineFromJson(json); + + /// Data layer bindings for [Machine]. + static Bindings bindings = Bindings( + fromJson: Machine.fromJson, + toJson: (obj) => obj.toJson(), + getId: (item) => item.id, + getDetailUrl: (id) => ApiUrl(path: 'machines/$id'), + getListUrl: () => const ApiUrl(path: 'machines'), + ); +} + +/// Extension for [Machine] lists. +extension MachineList on List { + /// Converts the list of orders into a map keyed by order id. + Map toIdsMap() { + return fold>( + {}, + (map, machine) { + map[machine.id] = machine; + return map; + }, + ); + } +} diff --git a/genlatte/genlatte-data/lib/src/models/machine.freezed.dart b/genlatte/genlatte-data/lib/src/models/machine.freezed.dart new file mode 100644 index 0000000..ed26b10 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/machine.freezed.dart @@ -0,0 +1,290 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'machine.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$Machine { + + String get id; String get name; bool get isActive; bool get isBlackAndWhite; +/// Create a copy of Machine +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$MachineCopyWith get copyWith => _$MachineCopyWithImpl(this as Machine, _$identity); + + /// Serializes this Machine to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Machine&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.isBlackAndWhite, isBlackAndWhite) || other.isBlackAndWhite == isBlackAndWhite)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,name,isActive,isBlackAndWhite); + +@override +String toString() { + return 'Machine(id: $id, name: $name, isActive: $isActive, isBlackAndWhite: $isBlackAndWhite)'; +} + + +} + +/// @nodoc +abstract mixin class $MachineCopyWith<$Res> { + factory $MachineCopyWith(Machine value, $Res Function(Machine) _then) = _$MachineCopyWithImpl; +@useResult +$Res call({ + String id, String name, bool isActive, bool isBlackAndWhite +}); + + + + +} +/// @nodoc +class _$MachineCopyWithImpl<$Res> + implements $MachineCopyWith<$Res> { + _$MachineCopyWithImpl(this._self, this._then); + + final Machine _self; + final $Res Function(Machine) _then; + +/// Create a copy of Machine +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? isActive = null,Object? isBlackAndWhite = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool,isBlackAndWhite: null == isBlackAndWhite ? _self.isBlackAndWhite : isBlackAndWhite // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [Machine]. +extension MachinePatterns on Machine { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Machine value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Machine() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Machine value) $default,){ +final _that = this; +switch (_that) { +case _Machine(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Machine value)? $default,){ +final _that = this; +switch (_that) { +case _Machine() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String name, bool isActive, bool isBlackAndWhite)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Machine() when $default != null: +return $default(_that.id,_that.name,_that.isActive,_that.isBlackAndWhite);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String name, bool isActive, bool isBlackAndWhite) $default,) {final _that = this; +switch (_that) { +case _Machine(): +return $default(_that.id,_that.name,_that.isActive,_that.isBlackAndWhite);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String name, bool isActive, bool isBlackAndWhite)? $default,) {final _that = this; +switch (_that) { +case _Machine() when $default != null: +return $default(_that.id,_that.name,_that.isActive,_that.isBlackAndWhite);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _Machine extends Machine { + const _Machine({required this.id, required this.name, this.isActive = true, this.isBlackAndWhite = true}): super._(); + factory _Machine.fromJson(Map json) => _$MachineFromJson(json); + +@override final String id; +@override final String name; +@override@JsonKey() final bool isActive; +@override@JsonKey() final bool isBlackAndWhite; + +/// Create a copy of Machine +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$MachineCopyWith<_Machine> get copyWith => __$MachineCopyWithImpl<_Machine>(this, _$identity); + +@override +Map toJson() { + return _$MachineToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Machine&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.isBlackAndWhite, isBlackAndWhite) || other.isBlackAndWhite == isBlackAndWhite)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,name,isActive,isBlackAndWhite); + +@override +String toString() { + return 'Machine(id: $id, name: $name, isActive: $isActive, isBlackAndWhite: $isBlackAndWhite)'; +} + + +} + +/// @nodoc +abstract mixin class _$MachineCopyWith<$Res> implements $MachineCopyWith<$Res> { + factory _$MachineCopyWith(_Machine value, $Res Function(_Machine) _then) = __$MachineCopyWithImpl; +@override @useResult +$Res call({ + String id, String name, bool isActive, bool isBlackAndWhite +}); + + + + +} +/// @nodoc +class __$MachineCopyWithImpl<$Res> + implements _$MachineCopyWith<$Res> { + __$MachineCopyWithImpl(this._self, this._then); + + final _Machine _self; + final $Res Function(_Machine) _then; + +/// Create a copy of Machine +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? isActive = null,Object? isBlackAndWhite = null,}) { + return _then(_Machine( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool,isBlackAndWhite: null == isBlackAndWhite ? _self.isBlackAndWhite : isBlackAndWhite // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-data/lib/src/models/machine.g.dart b/genlatte/genlatte-data/lib/src/models/machine.g.dart new file mode 100644 index 0000000..79b72e4 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/machine.g.dart @@ -0,0 +1,25 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'machine.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_Machine _$MachineFromJson(Map json) => _Machine( + id: json['id'] as String, + name: json['name'] as String, + isActive: json['isActive'] as bool? ?? true, + isBlackAndWhite: json['isBlackAndWhite'] as bool? ?? true, +); + +Map _$MachineToJson(_Machine instance) => { + 'id': instance.id, + 'name': instance.name, + 'isActive': instance.isActive, + 'isBlackAndWhite': instance.isBlackAndWhite, +}; diff --git a/genlatte/genlatte-data/lib/src/models/models.dart b/genlatte/genlatte-data/lib/src/models/models.dart new file mode 100644 index 0000000..6884cf7 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/models.dart @@ -0,0 +1,10 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'barista.dart'; +export 'latte_image.dart'; +export 'latte_options.dart'; +export 'latte_order.dart'; +export 'machine.dart'; +export 'question.dart'; diff --git a/genlatte/genlatte-data/lib/src/models/question.dart b/genlatte/genlatte-data/lib/src/models/question.dart new file mode 100644 index 0000000..4fe030e --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/question.dart @@ -0,0 +1,112 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'question.freezed.dart'; +part 'question.g.dart'; + +/// Question which informs one detail of a coffee or its latte art. +@Freezed(unionKey: 'type') +sealed class Question with _$Question { + /// Simple text-based input, used to ask the user their name or for their + /// happy place. + @FreezedUnionValue('textQuestion') + const factory Question({ + required String id, + @JsonKey(name: 'question') // + required String body, + @Default('Enter your answer') String helpText, + String? answer, + }) = TextQuestion; + + /// Standard multiple choice question, which are provided via + /// [acceptableAnswers]. + @FreezedUnionValue('multipleChoiceQuestion') + const factory Question.multipleChoice({ + required String id, + + @JsonKey(name: 'question') // + required String body, + + required List acceptableAnswers, + String? selectedValue, + }) = MultipleChoiceQuestion; + + /// Simple end-to-end slider, with bookends like "Light" and "Dark". + @FreezedUnionValue('zeroToOneQuestion') + const factory Question.zeroToOne({ + required String id, + @JsonKey(name: 'question') // + required String body, + required String minValueLabel, + required String maxValueLabel, + + /// A value of 0 to 1, representing the value's place between the two + /// labels. + double? selectedValue, + }) = ZeroToOneQuestion; + + /// Slider to shift a value relative to the current perceived value in the + /// image, with values like "Lighter" and "Darker", and which will show + /// "No change" above the midpart of the slider. + @FreezedUnionValue('negativeOneToOneQuestion') + const factory Question.negativeOneToOne({ + required String id, + @JsonKey(name: 'question') // + required String body, + required String minValueLabel, + required String maxValueLabel, + + /// A value of -1 to 1, representing the value's shift relative to the + /// user's perceived value in the original image. + double? selectedValue, + }) = NegativeOneToOneQuestion; + + const Question._(); + + factory Question.fromJson(Map json) => + _$QuestionFromJson(json); + + /// Returns a copy of this question with the given [answer] set. + Question copyWithAnswer(Object? answer) => switch (this) { + MultipleChoiceQuestion() => (this as MultipleChoiceQuestion).copyWith( + selectedValue: answer as String?, + ), + ZeroToOneQuestion() => (this as ZeroToOneQuestion).copyWith( + selectedValue: answer as double?, + ), + NegativeOneToOneQuestion() => (this as NegativeOneToOneQuestion).copyWith( + selectedValue: answer as double?, + ), + TextQuestion() => (this as TextQuestion).copyWith( + answer: answer as String?, + ), + }; + + /// The answer to this question, if it has one. + Object? get answer => switch (this) { + MultipleChoiceQuestion(:final selectedValue) => selectedValue, + ZeroToOneQuestion(:final selectedValue) => selectedValue, + NegativeOneToOneQuestion(:final selectedValue) => selectedValue, + TextQuestion(:final answer) => answer, + }; + + /// True if this question's answer is non-null. + bool get isAnswered => answer != null; +} + +/// {@template QuestionConverter} +/// Json converter for [Question]. +/// {@endtemplate} +class QuestionConverter extends JsonConverter> { + /// {@macro QuestionConverter} + const QuestionConverter(); + + @override + Question fromJson(Map json) => Question.fromJson(json); + + @override + Map toJson(Question object) => object.toJson(); +} diff --git a/genlatte/genlatte-data/lib/src/models/question.freezed.dart b/genlatte/genlatte-data/lib/src/models/question.freezed.dart new file mode 100644 index 0000000..74fc75f --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/question.freezed.dart @@ -0,0 +1,586 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'question.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +Question _$QuestionFromJson( + Map json +) { + switch (json['type']) { + case 'textQuestion': + return TextQuestion.fromJson( + json + ); + case 'multipleChoiceQuestion': + return MultipleChoiceQuestion.fromJson( + json + ); + case 'zeroToOneQuestion': + return ZeroToOneQuestion.fromJson( + json + ); + case 'negativeOneToOneQuestion': + return NegativeOneToOneQuestion.fromJson( + json + ); + + default: + throw CheckedFromJsonException( + json, + 'type', + 'Question', + 'Invalid union type "${json['type']}"!' +); + } + +} + +/// @nodoc +mixin _$Question { + + String get id;@JsonKey(name: 'question') String get body; +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QuestionCopyWith get copyWith => _$QuestionCopyWithImpl(this as Question, _$identity); + + /// Serializes this Question to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Question&&(identical(other.id, id) || other.id == id)&&(identical(other.body, body) || other.body == body)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,body); + +@override +String toString() { + return 'Question(id: $id, body: $body)'; +} + + +} + +/// @nodoc +abstract mixin class $QuestionCopyWith<$Res> { + factory $QuestionCopyWith(Question value, $Res Function(Question) _then) = _$QuestionCopyWithImpl; +@useResult +$Res call({ + String id,@JsonKey(name: 'question') String body +}); + + + + +} +/// @nodoc +class _$QuestionCopyWithImpl<$Res> + implements $QuestionCopyWith<$Res> { + _$QuestionCopyWithImpl(this._self, this._then); + + final Question _self; + final $Res Function(Question) _then; + +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? body = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [Question]. +extension QuestionPatterns on Question { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( TextQuestion value)? $default,{TResult Function( MultipleChoiceQuestion value)? multipleChoice,TResult Function( ZeroToOneQuestion value)? zeroToOne,TResult Function( NegativeOneToOneQuestion value)? negativeOneToOne,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case TextQuestion() when $default != null: +return $default(_that);case MultipleChoiceQuestion() when multipleChoice != null: +return multipleChoice(_that);case ZeroToOneQuestion() when zeroToOne != null: +return zeroToOne(_that);case NegativeOneToOneQuestion() when negativeOneToOne != null: +return negativeOneToOne(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( TextQuestion value) $default,{required TResult Function( MultipleChoiceQuestion value) multipleChoice,required TResult Function( ZeroToOneQuestion value) zeroToOne,required TResult Function( NegativeOneToOneQuestion value) negativeOneToOne,}){ +final _that = this; +switch (_that) { +case TextQuestion(): +return $default(_that);case MultipleChoiceQuestion(): +return multipleChoice(_that);case ZeroToOneQuestion(): +return zeroToOne(_that);case NegativeOneToOneQuestion(): +return negativeOneToOne(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( TextQuestion value)? $default,{TResult? Function( MultipleChoiceQuestion value)? multipleChoice,TResult? Function( ZeroToOneQuestion value)? zeroToOne,TResult? Function( NegativeOneToOneQuestion value)? negativeOneToOne,}){ +final _that = this; +switch (_that) { +case TextQuestion() when $default != null: +return $default(_that);case MultipleChoiceQuestion() when multipleChoice != null: +return multipleChoice(_that);case ZeroToOneQuestion() when zeroToOne != null: +return zeroToOne(_that);case NegativeOneToOneQuestion() when negativeOneToOne != null: +return negativeOneToOne(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, @JsonKey(name: 'question') String body, String helpText, String? answer)? $default,{TResult Function( String id, @JsonKey(name: 'question') String body, List acceptableAnswers, String? selectedValue)? multipleChoice,TResult Function( String id, @JsonKey(name: 'question') String body, String minValueLabel, String maxValueLabel, double? selectedValue)? zeroToOne,TResult Function( String id, @JsonKey(name: 'question') String body, String minValueLabel, String maxValueLabel, double? selectedValue)? negativeOneToOne,required TResult orElse(),}) {final _that = this; +switch (_that) { +case TextQuestion() when $default != null: +return $default(_that.id,_that.body,_that.helpText,_that.answer);case MultipleChoiceQuestion() when multipleChoice != null: +return multipleChoice(_that.id,_that.body,_that.acceptableAnswers,_that.selectedValue);case ZeroToOneQuestion() when zeroToOne != null: +return zeroToOne(_that.id,_that.body,_that.minValueLabel,_that.maxValueLabel,_that.selectedValue);case NegativeOneToOneQuestion() when negativeOneToOne != null: +return negativeOneToOne(_that.id,_that.body,_that.minValueLabel,_that.maxValueLabel,_that.selectedValue);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, @JsonKey(name: 'question') String body, String helpText, String? answer) $default,{required TResult Function( String id, @JsonKey(name: 'question') String body, List acceptableAnswers, String? selectedValue) multipleChoice,required TResult Function( String id, @JsonKey(name: 'question') String body, String minValueLabel, String maxValueLabel, double? selectedValue) zeroToOne,required TResult Function( String id, @JsonKey(name: 'question') String body, String minValueLabel, String maxValueLabel, double? selectedValue) negativeOneToOne,}) {final _that = this; +switch (_that) { +case TextQuestion(): +return $default(_that.id,_that.body,_that.helpText,_that.answer);case MultipleChoiceQuestion(): +return multipleChoice(_that.id,_that.body,_that.acceptableAnswers,_that.selectedValue);case ZeroToOneQuestion(): +return zeroToOne(_that.id,_that.body,_that.minValueLabel,_that.maxValueLabel,_that.selectedValue);case NegativeOneToOneQuestion(): +return negativeOneToOne(_that.id,_that.body,_that.minValueLabel,_that.maxValueLabel,_that.selectedValue);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, @JsonKey(name: 'question') String body, String helpText, String? answer)? $default,{TResult? Function( String id, @JsonKey(name: 'question') String body, List acceptableAnswers, String? selectedValue)? multipleChoice,TResult? Function( String id, @JsonKey(name: 'question') String body, String minValueLabel, String maxValueLabel, double? selectedValue)? zeroToOne,TResult? Function( String id, @JsonKey(name: 'question') String body, String minValueLabel, String maxValueLabel, double? selectedValue)? negativeOneToOne,}) {final _that = this; +switch (_that) { +case TextQuestion() when $default != null: +return $default(_that.id,_that.body,_that.helpText,_that.answer);case MultipleChoiceQuestion() when multipleChoice != null: +return multipleChoice(_that.id,_that.body,_that.acceptableAnswers,_that.selectedValue);case ZeroToOneQuestion() when zeroToOne != null: +return zeroToOne(_that.id,_that.body,_that.minValueLabel,_that.maxValueLabel,_that.selectedValue);case NegativeOneToOneQuestion() when negativeOneToOne != null: +return negativeOneToOne(_that.id,_that.body,_that.minValueLabel,_that.maxValueLabel,_that.selectedValue);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class TextQuestion extends Question { + const TextQuestion({required this.id, @JsonKey(name: 'question') required this.body, this.helpText = 'Enter your answer', this.answer, final String? $type}): $type = $type ?? 'textQuestion',super._(); + factory TextQuestion.fromJson(Map json) => _$TextQuestionFromJson(json); + +@override final String id; +@override@JsonKey(name: 'question') final String body; +@JsonKey() final String helpText; + final String? answer; + +@JsonKey(name: 'type') +final String $type; + + +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$TextQuestionCopyWith get copyWith => _$TextQuestionCopyWithImpl(this, _$identity); + +@override +Map toJson() { + return _$TextQuestionToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is TextQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.body, body) || other.body == body)&&(identical(other.helpText, helpText) || other.helpText == helpText)&&(identical(other.answer, answer) || other.answer == answer)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,body,helpText,answer); + +@override +String toString() { + return 'Question(id: $id, body: $body, helpText: $helpText, answer: $answer)'; +} + + +} + +/// @nodoc +abstract mixin class $TextQuestionCopyWith<$Res> implements $QuestionCopyWith<$Res> { + factory $TextQuestionCopyWith(TextQuestion value, $Res Function(TextQuestion) _then) = _$TextQuestionCopyWithImpl; +@override @useResult +$Res call({ + String id,@JsonKey(name: 'question') String body, String helpText, String? answer +}); + + + + +} +/// @nodoc +class _$TextQuestionCopyWithImpl<$Res> + implements $TextQuestionCopyWith<$Res> { + _$TextQuestionCopyWithImpl(this._self, this._then); + + final TextQuestion _self; + final $Res Function(TextQuestion) _then; + +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? body = null,Object? helpText = null,Object? answer = freezed,}) { + return _then(TextQuestion( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,helpText: null == helpText ? _self.helpText : helpText // ignore: cast_nullable_to_non_nullable +as String,answer: freezed == answer ? _self.answer : answer // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +/// @nodoc +@JsonSerializable() + +class MultipleChoiceQuestion extends Question { + const MultipleChoiceQuestion({required this.id, @JsonKey(name: 'question') required this.body, required final List acceptableAnswers, this.selectedValue, final String? $type}): _acceptableAnswers = acceptableAnswers,$type = $type ?? 'multipleChoiceQuestion',super._(); + factory MultipleChoiceQuestion.fromJson(Map json) => _$MultipleChoiceQuestionFromJson(json); + +@override final String id; +@override@JsonKey(name: 'question') final String body; + final List _acceptableAnswers; + List get acceptableAnswers { + if (_acceptableAnswers is EqualUnmodifiableListView) return _acceptableAnswers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_acceptableAnswers); +} + + final String? selectedValue; + +@JsonKey(name: 'type') +final String $type; + + +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$MultipleChoiceQuestionCopyWith get copyWith => _$MultipleChoiceQuestionCopyWithImpl(this, _$identity); + +@override +Map toJson() { + return _$MultipleChoiceQuestionToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is MultipleChoiceQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.body, body) || other.body == body)&&const DeepCollectionEquality().equals(other._acceptableAnswers, _acceptableAnswers)&&(identical(other.selectedValue, selectedValue) || other.selectedValue == selectedValue)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,body,const DeepCollectionEquality().hash(_acceptableAnswers),selectedValue); + +@override +String toString() { + return 'Question.multipleChoice(id: $id, body: $body, acceptableAnswers: $acceptableAnswers, selectedValue: $selectedValue)'; +} + + +} + +/// @nodoc +abstract mixin class $MultipleChoiceQuestionCopyWith<$Res> implements $QuestionCopyWith<$Res> { + factory $MultipleChoiceQuestionCopyWith(MultipleChoiceQuestion value, $Res Function(MultipleChoiceQuestion) _then) = _$MultipleChoiceQuestionCopyWithImpl; +@override @useResult +$Res call({ + String id,@JsonKey(name: 'question') String body, List acceptableAnswers, String? selectedValue +}); + + + + +} +/// @nodoc +class _$MultipleChoiceQuestionCopyWithImpl<$Res> + implements $MultipleChoiceQuestionCopyWith<$Res> { + _$MultipleChoiceQuestionCopyWithImpl(this._self, this._then); + + final MultipleChoiceQuestion _self; + final $Res Function(MultipleChoiceQuestion) _then; + +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? body = null,Object? acceptableAnswers = null,Object? selectedValue = freezed,}) { + return _then(MultipleChoiceQuestion( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,acceptableAnswers: null == acceptableAnswers ? _self._acceptableAnswers : acceptableAnswers // ignore: cast_nullable_to_non_nullable +as List,selectedValue: freezed == selectedValue ? _self.selectedValue : selectedValue // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +/// @nodoc +@JsonSerializable() + +class ZeroToOneQuestion extends Question { + const ZeroToOneQuestion({required this.id, @JsonKey(name: 'question') required this.body, required this.minValueLabel, required this.maxValueLabel, this.selectedValue, final String? $type}): $type = $type ?? 'zeroToOneQuestion',super._(); + factory ZeroToOneQuestion.fromJson(Map json) => _$ZeroToOneQuestionFromJson(json); + +@override final String id; +@override@JsonKey(name: 'question') final String body; + final String minValueLabel; + final String maxValueLabel; +/// A value of 0 to 1, representing the value's place between the two +/// labels. + final double? selectedValue; + +@JsonKey(name: 'type') +final String $type; + + +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ZeroToOneQuestionCopyWith get copyWith => _$ZeroToOneQuestionCopyWithImpl(this, _$identity); + +@override +Map toJson() { + return _$ZeroToOneQuestionToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ZeroToOneQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.body, body) || other.body == body)&&(identical(other.minValueLabel, minValueLabel) || other.minValueLabel == minValueLabel)&&(identical(other.maxValueLabel, maxValueLabel) || other.maxValueLabel == maxValueLabel)&&(identical(other.selectedValue, selectedValue) || other.selectedValue == selectedValue)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,body,minValueLabel,maxValueLabel,selectedValue); + +@override +String toString() { + return 'Question.zeroToOne(id: $id, body: $body, minValueLabel: $minValueLabel, maxValueLabel: $maxValueLabel, selectedValue: $selectedValue)'; +} + + +} + +/// @nodoc +abstract mixin class $ZeroToOneQuestionCopyWith<$Res> implements $QuestionCopyWith<$Res> { + factory $ZeroToOneQuestionCopyWith(ZeroToOneQuestion value, $Res Function(ZeroToOneQuestion) _then) = _$ZeroToOneQuestionCopyWithImpl; +@override @useResult +$Res call({ + String id,@JsonKey(name: 'question') String body, String minValueLabel, String maxValueLabel, double? selectedValue +}); + + + + +} +/// @nodoc +class _$ZeroToOneQuestionCopyWithImpl<$Res> + implements $ZeroToOneQuestionCopyWith<$Res> { + _$ZeroToOneQuestionCopyWithImpl(this._self, this._then); + + final ZeroToOneQuestion _self; + final $Res Function(ZeroToOneQuestion) _then; + +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? body = null,Object? minValueLabel = null,Object? maxValueLabel = null,Object? selectedValue = freezed,}) { + return _then(ZeroToOneQuestion( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,minValueLabel: null == minValueLabel ? _self.minValueLabel : minValueLabel // ignore: cast_nullable_to_non_nullable +as String,maxValueLabel: null == maxValueLabel ? _self.maxValueLabel : maxValueLabel // ignore: cast_nullable_to_non_nullable +as String,selectedValue: freezed == selectedValue ? _self.selectedValue : selectedValue // ignore: cast_nullable_to_non_nullable +as double?, + )); +} + + +} + +/// @nodoc +@JsonSerializable() + +class NegativeOneToOneQuestion extends Question { + const NegativeOneToOneQuestion({required this.id, @JsonKey(name: 'question') required this.body, required this.minValueLabel, required this.maxValueLabel, this.selectedValue, final String? $type}): $type = $type ?? 'negativeOneToOneQuestion',super._(); + factory NegativeOneToOneQuestion.fromJson(Map json) => _$NegativeOneToOneQuestionFromJson(json); + +@override final String id; +@override@JsonKey(name: 'question') final String body; + final String minValueLabel; + final String maxValueLabel; +/// A value of -1 to 1, representing the value's shift relative to the +/// user's perceived value in the original image. + final double? selectedValue; + +@JsonKey(name: 'type') +final String $type; + + +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NegativeOneToOneQuestionCopyWith get copyWith => _$NegativeOneToOneQuestionCopyWithImpl(this, _$identity); + +@override +Map toJson() { + return _$NegativeOneToOneQuestionToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NegativeOneToOneQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.body, body) || other.body == body)&&(identical(other.minValueLabel, minValueLabel) || other.minValueLabel == minValueLabel)&&(identical(other.maxValueLabel, maxValueLabel) || other.maxValueLabel == maxValueLabel)&&(identical(other.selectedValue, selectedValue) || other.selectedValue == selectedValue)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,body,minValueLabel,maxValueLabel,selectedValue); + +@override +String toString() { + return 'Question.negativeOneToOne(id: $id, body: $body, minValueLabel: $minValueLabel, maxValueLabel: $maxValueLabel, selectedValue: $selectedValue)'; +} + + +} + +/// @nodoc +abstract mixin class $NegativeOneToOneQuestionCopyWith<$Res> implements $QuestionCopyWith<$Res> { + factory $NegativeOneToOneQuestionCopyWith(NegativeOneToOneQuestion value, $Res Function(NegativeOneToOneQuestion) _then) = _$NegativeOneToOneQuestionCopyWithImpl; +@override @useResult +$Res call({ + String id,@JsonKey(name: 'question') String body, String minValueLabel, String maxValueLabel, double? selectedValue +}); + + + + +} +/// @nodoc +class _$NegativeOneToOneQuestionCopyWithImpl<$Res> + implements $NegativeOneToOneQuestionCopyWith<$Res> { + _$NegativeOneToOneQuestionCopyWithImpl(this._self, this._then); + + final NegativeOneToOneQuestion _self; + final $Res Function(NegativeOneToOneQuestion) _then; + +/// Create a copy of Question +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? body = null,Object? minValueLabel = null,Object? maxValueLabel = null,Object? selectedValue = freezed,}) { + return _then(NegativeOneToOneQuestion( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,minValueLabel: null == minValueLabel ? _self.minValueLabel : minValueLabel // ignore: cast_nullable_to_non_nullable +as String,maxValueLabel: null == maxValueLabel ? _self.maxValueLabel : maxValueLabel // ignore: cast_nullable_to_non_nullable +as String,selectedValue: freezed == selectedValue ? _self.selectedValue : selectedValue // ignore: cast_nullable_to_non_nullable +as double?, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-data/lib/src/models/question.g.dart b/genlatte/genlatte-data/lib/src/models/question.g.dart new file mode 100644 index 0000000..f2ca892 --- /dev/null +++ b/genlatte/genlatte-data/lib/src/models/question.g.dart @@ -0,0 +1,92 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'question.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TextQuestion _$TextQuestionFromJson(Map json) => TextQuestion( + id: json['id'] as String, + body: json['question'] as String, + helpText: json['helpText'] as String? ?? 'Enter your answer', + answer: json['answer'] as String?, + $type: json['type'] as String?, +); + +Map _$TextQuestionToJson(TextQuestion instance) => + { + 'id': instance.id, + 'question': instance.body, + 'helpText': instance.helpText, + 'answer': instance.answer, + 'type': instance.$type, + }; + +MultipleChoiceQuestion _$MultipleChoiceQuestionFromJson( + Map json, +) => MultipleChoiceQuestion( + id: json['id'] as String, + body: json['question'] as String, + acceptableAnswers: (json['acceptableAnswers'] as List) + .map((e) => e as String) + .toList(), + selectedValue: json['selectedValue'] as String?, + $type: json['type'] as String?, +); + +Map _$MultipleChoiceQuestionToJson( + MultipleChoiceQuestion instance, +) => { + 'id': instance.id, + 'question': instance.body, + 'acceptableAnswers': instance.acceptableAnswers, + 'selectedValue': instance.selectedValue, + 'type': instance.$type, +}; + +ZeroToOneQuestion _$ZeroToOneQuestionFromJson(Map json) => + ZeroToOneQuestion( + id: json['id'] as String, + body: json['question'] as String, + minValueLabel: json['minValueLabel'] as String, + maxValueLabel: json['maxValueLabel'] as String, + selectedValue: (json['selectedValue'] as num?)?.toDouble(), + $type: json['type'] as String?, + ); + +Map _$ZeroToOneQuestionToJson(ZeroToOneQuestion instance) => + { + 'id': instance.id, + 'question': instance.body, + 'minValueLabel': instance.minValueLabel, + 'maxValueLabel': instance.maxValueLabel, + 'selectedValue': instance.selectedValue, + 'type': instance.$type, + }; + +NegativeOneToOneQuestion _$NegativeOneToOneQuestionFromJson( + Map json, +) => NegativeOneToOneQuestion( + id: json['id'] as String, + body: json['question'] as String, + minValueLabel: json['minValueLabel'] as String, + maxValueLabel: json['maxValueLabel'] as String, + selectedValue: (json['selectedValue'] as num?)?.toDouble(), + $type: json['type'] as String?, +); + +Map _$NegativeOneToOneQuestionToJson( + NegativeOneToOneQuestion instance, +) => { + 'id': instance.id, + 'question': instance.body, + 'minValueLabel': instance.minValueLabel, + 'maxValueLabel': instance.maxValueLabel, + 'selectedValue': instance.selectedValue, + 'type': instance.$type, +}; diff --git a/genlatte/genlatte-data/pubspec.yaml b/genlatte/genlatte-data/pubspec.yaml new file mode 100644 index 0000000..c3ce2bf --- /dev/null +++ b/genlatte/genlatte-data/pubspec.yaml @@ -0,0 +1,20 @@ +name: genlatte_data +description: A starting point for Dart libraries or applications. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo +environment: + sdk: ^3.11.0-296.4.beta + +# Add regular dependencies here. +dependencies: + data_layer: 0.0.5 + freezed_annotation: ^3.1.0 + json_annotation: ^4.10.0 + +dev_dependencies: + build_runner: ^2.10.5 + freezed: ^3.2.4 + json_serializable: ^6.12.0 + mocktail: ^1.0.4 + test: ^1.25.6 + very_good_analysis: ^10.0.0 diff --git a/genlatte/genlatte-data/test/CONTEXT.md b/genlatte/genlatte-data/test/CONTEXT.md new file mode 100644 index 0000000..b198d89 --- /dev/null +++ b/genlatte/genlatte-data/test/CONTEXT.md @@ -0,0 +1,4 @@ +# genlatte-data Tests + +**Purpose:** +Unit verification encompassing data models properties and behavior parity. diff --git a/genlatte/genlatte-data/test/models/CONTEXT.md b/genlatte/genlatte-data/test/models/CONTEXT.md new file mode 100644 index 0000000..98a4426 --- /dev/null +++ b/genlatte/genlatte-data/test/models/CONTEXT.md @@ -0,0 +1,4 @@ +# genlatte-data Model Tests + +**Purpose:** +Tests verifying JsonSerialization implementations for nested Freezed records (e.g. `LatteOrder`). diff --git a/genlatte/genlatte-data/test/models/barista_test.dart b/genlatte/genlatte-data/test/models/barista_test.dart new file mode 100644 index 0000000..02695de --- /dev/null +++ b/genlatte/genlatte-data/test/models/barista_test.dart @@ -0,0 +1,42 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte_data/models.dart'; +import 'package:test/test.dart'; + +void main() { + group('Barista Serialization', () { + test('JSON serialization is correct for a Barista', () { + final json = { + 'id': 'b123', + 'username': 'jon_doe', + 'persona': 'asianMale', + }; + + final barista = Barista.fromJson(json); + + expect(barista.id, 'b123'); + expect(barista.username, 'jon_doe'); + expect(barista.persona, BaristaPersona.asianMale); + + final serialized = barista.toJson(); + expect(serialized['id'], 'b123'); + expect(serialized['username'], 'jon_doe'); + expect(serialized['persona'], 'asianMale'); + }); + + test('JSON serialization for Barista without id', () { + final json = { + 'username': 'jane_doe', + 'persona': 'caucasianFemale', + }; + + final barista = Barista.fromJson(json); + + expect(barista.id, isNull); + expect(barista.username, 'jane_doe'); + expect(barista.persona, BaristaPersona.caucasianFemale); + }); + }); +} diff --git a/genlatte/genlatte-data/test/models/latte_image_test.dart b/genlatte/genlatte-data/test/models/latte_image_test.dart new file mode 100644 index 0000000..30836c8 --- /dev/null +++ b/genlatte/genlatte-data/test/models/latte_image_test.dart @@ -0,0 +1,228 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte_data/models.dart'; +import 'package:test/test.dart'; + +void main() { + group('LatteImageBatchParent Serialization', () { + test('JSON serialization is correct for a LatteImageBatchParent', () { + final json = { + 'id': 'batch1', + 'imageIndex': 'image1', + }; + + final parent = LatteImageBatchParent.fromJson(json); + + expect(parent.id, 'batch1'); + expect(parent.imageIndex, 'image1'); + + final serialized = parent.toJson(); + expect(serialized['id'], 'batch1'); + expect(serialized['imageIndex'], 'image1'); + }); + }); + + group('LatteImage Serialization', () { + test('JSON serialization is correct for a LatteImage', () { + final json = { + 'imageUrl': 'https://example.com/latte.jpg', + 'prompt': 'A beautiful swan in coffee', + 'description': 'A beautiful swan', + 'questions': [ + { + 'type': 'textQuestion', + 'id': 'q1', + 'question': 'What would you name it?', + 'helpText': 'Enter your answer', + }, + ], + }; + + final image = LatteImage.fromJson(json); + + expect(image.imageUrl, 'https://example.com/latte.jpg'); + expect(image.prompt, 'A beautiful swan in coffee'); + expect(image.description, 'A beautiful swan'); + expect(image.questions?.length, 1); + final q = image.questions!.first; + expect(q, isA()); + expect((q as TextQuestion).body, 'What would you name it?'); + + final serialized = image.toJson(); + expect(serialized['imageUrl'], 'https://example.com/latte.jpg'); + expect(serialized['prompt'], 'A beautiful swan in coffee'); + expect( + (serialized['questions'] as List>).first['type'], + 'textQuestion', + ); + }); + }); + + group('LatteImageBatch Serialization', () { + test('JSON serialization is correct for a LatteImageBatch', () { + final json = { + 'id': 'batch001', + 'orderId': 'order001', + 'image0': { + 'imageUrl': 'https://example.com/0.jpg', + 'prompt': 'Prompt 0', + 'description': 'Desc 0', + 'questions': null, + }, + 'image1': { + 'imageUrl': 'https://example.com/1.jpg', + 'prompt': 'Prompt 1', + 'description': 'Desc 1', + 'questions': [], + }, + 'image2': null, + 'image3': null, + 'parent': {'id': 'batch000', 'imageIndex': 'image2'}, + }; + + final batch = LatteImageBatch.fromJson(json); + + expect(batch.id, 'batch001'); + expect(batch.orderId, 'order001'); + expect(batch.image0?.imageUrl, 'https://example.com/0.jpg'); + expect(batch.image1?.prompt, 'Prompt 1'); + expect(batch.image2, isNull); + expect(batch.parent?.imageIndex, 'image2'); + + final serialized = batch.toJson(); + expect(serialized['id'], 'batch001'); + expect(serialized['orderId'], 'order001'); + expect( + (serialized['image0'] as Map)['imageUrl'], + 'https://example.com/0.jpg', + ); + expect(serialized['image2'], isNull); + expect((serialized['parent'] as Map)['imageIndex'], 'image2'); + }); + }); + + group('LatteImage answerQuestion', () { + const defaultQuestion = Question.multipleChoice( + id: 'q1', + body: 'Size?', + acceptableAnswers: ['Small', 'Large'], + ); + const textQuestion = Question( + id: 'q2', + body: 'Name?', + ); + const img = LatteImage( + imageUrl: 'url', + prompt: 'prompt', + description: 'desc', + questions: [defaultQuestion, textQuestion], + ); + + test('updates the correct question with the given answer', () { + final updatedImg = img.answerQuestion(defaultQuestion, 'Large'); + + expect( + (updatedImg.questions![0] as MultipleChoiceQuestion).selectedValue, + 'Large', + ); + expect((updatedImg.questions![1] as TextQuestion).answer, isNull); + }); + + test('throws if questions list is null or empty', () { + const emptyImg = LatteImage( + imageUrl: 'url', + prompt: 'prompt', + description: 'desc', + questions: null, + ); + + expect( + () => emptyImg.answerQuestion(defaultQuestion, 'answer'), + throwsA(isA()), + ); + }); + + test('throws if question is not found in the list', () { + const notFoundQuestion = Question(id: 'q3', body: 'Missing?'); + expect( + () => img.answerQuestion(notFoundQuestion, 'answer'), + throwsException, + ); + }); + }); + + group('LatteImageBatch answerQuestion', () { + const q = Question(id: 'q1', body: 'Test?'); + const imgTemplate = LatteImage( + imageUrl: 'url', + prompt: 'prompt', + description: 'desc', + questions: [q], + ); + const batch = LatteImageBatch( + id: 'batch1', + orderId: 'order1', + image0: imgTemplate, + image1: imgTemplate, + image2: imgTemplate, + image3: imgTemplate, + ); + + test('updates image0 if index is 0', () { + final updatedBatch = batch.answerQuestion(0, q, 'ans0'); + expect( + (updatedBatch.image0!.questions!.first as TextQuestion).answer, + 'ans0', + ); + expect( + (updatedBatch.image1!.questions!.first as TextQuestion).answer, + isNull, + ); + }); + + test('updates image1 if index is 1', () { + final updatedBatch = batch.answerQuestion(1, q, 'ans1'); + expect( + (updatedBatch.image1!.questions!.first as TextQuestion).answer, + 'ans1', + ); + expect( + (updatedBatch.image0!.questions!.first as TextQuestion).answer, + isNull, + ); + }); + + test('updates image2 if index is 2', () { + final updatedBatch = batch.answerQuestion(2, q, 'ans2'); + expect( + (updatedBatch.image2!.questions!.first as TextQuestion).answer, + 'ans2', + ); + expect( + (updatedBatch.image3!.questions!.first as TextQuestion).answer, + isNull, + ); + }); + + test('updates image3 if index is 3', () { + final updatedBatch = batch.answerQuestion(3, q, 'ans3'); + expect( + (updatedBatch.image3!.questions!.first as TextQuestion).answer, + 'ans3', + ); + expect( + (updatedBatch.image2!.questions!.first as TextQuestion).answer, + isNull, + ); + }); + + test('throws RangeError for invalid index', () { + expect( + () => batch.answerQuestion(4, q, 'ans'), + throwsRangeError, + ); + }); + }); +} diff --git a/genlatte/genlatte-data/test/models/latte_options_test.dart b/genlatte/genlatte-data/test/models/latte_options_test.dart new file mode 100644 index 0000000..fbde428 --- /dev/null +++ b/genlatte/genlatte-data/test/models/latte_options_test.dart @@ -0,0 +1,42 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:data_layer/data_layer.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:test/test.dart'; + +void main() { + group('LatteOptions Serialization', () { + test('JSON serialization is correct for a LatteOptions', () { + final json = { + 'id': 'milk', + 'values': [ + {'name': 'Oat'}, + {'name': 'Almond'}, + {'name': 'Whole'}, + ], + }; + + final options = LatteOptions.fromJson(json); + + expect(options.id, 'milk'); + expect(options.values, [ + const LatteOption(name: 'Oat'), + const LatteOption(name: 'Almond'), + const LatteOption(name: 'Whole'), + ]); + + final serialized = options.toJson(); + expect(serialized['id'], 'milk'); + expect( + serialized['values'], + [ + {'name': 'Oat', 'isAvailable': true}, + {'name': 'Almond', 'isAvailable': true}, + {'name': 'Whole', 'isAvailable': true}, + ], + ); + }); + }); +} diff --git a/genlatte/genlatte-data/test/models/latte_order_test.dart b/genlatte/genlatte-data/test/models/latte_order_test.dart new file mode 100644 index 0000000..4108ec2 --- /dev/null +++ b/genlatte/genlatte-data/test/models/latte_order_test.dart @@ -0,0 +1,88 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte_data/models.dart'; +import 'package:test/test.dart'; + +void main() { + group('LatteOrder Serialization', () { + test('JSON serialization is correct for a fully populated order', () { + final json = { + 'id': 'order-123', + 'name': 'Alice', + 'milk': 'Oat', + 'sweetener': 'Vanilla', + 'happyPlace': 'A cozy cabin', + }; + + final order = LatteOrder.fromJson(json); + + expect(order.id, 'order-123'); + expect(order.name, 'Alice'); + expect(order.milk, 'Oat'); + expect(order.sweetener, 'Vanilla'); + expect(order.happyPlace, 'A cozy cabin'); + + expect(order.toJson(), json); + }); + + test('JSON serialization is correct for an empty order', () { + final json = { + 'id': null, + 'name': null, + 'milk': null, + 'sweetener': null, + 'happyPlace': null, + }; + + final order = LatteOrder.fromJson(json); + + expect(order.id, isNull); + expect(order.name, isNull); + + // Nulls are generally excluded or included depending on json_serializable + // config. But we can verify toJson includes what's expected. We should + // match the actual object property Let's just create an empty order and + // see its toJson. + const emptyOrder = LatteOrder(); + + // We expect unpopulated fields to either serialize as null or be omitted. + // Since freezed creates `id?: String` etc, the json will likely have the + // keys if includeIfNull: true We will assert on the required structure. + final serialized = emptyOrder.toJson(); + // Only verifying fields that actually exist or not throwing error + expect(serialized.containsKey('id'), isTrue); + }); + }); + + group('LatteOrderMetadata Serialization', () { + test('JSON serialization is correct for a fully populated metadata', () { + final json = { + 'id': 'meta-123', + 'orderNumber': 42, + 'isNameApproved': true, + 'isHappyPlaceApproved': false, + 'happyPlaceModerationReason': 'barista_moderation', + 'isImageApproved': true, + 'imageBatchId': 'batch-001', + 'imageUrl': 'https://example.com/image.png', + 'status': 'submitted', + 'baristaId': 'barista-jon', + 'orderSubmittedTime': '2023-10-10T10:10:10.000Z', + 'completionTime': '2023-10-10T10:15:10.000Z', + }; + + final metadata = LatteOrderMetadata.fromJson(json); + + expect(metadata.id, 'meta-123'); + expect(metadata.orderNumber, 42); + expect(metadata.status, LatteOrderStatus.submitted); + + final serialized = metadata.toJson(); + expect(serialized['id'], 'meta-123'); + expect(serialized['status'], 'submitted'); + expect(serialized['orderSubmittedTime'], '2023-10-10T10:10:10.000Z'); + }); + }); +} diff --git a/genlatte/genlatte-data/test/models/question_test.dart b/genlatte/genlatte-data/test/models/question_test.dart new file mode 100644 index 0000000..359d790 --- /dev/null +++ b/genlatte/genlatte-data/test/models/question_test.dart @@ -0,0 +1,105 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte_data/models.dart'; +import 'package:test/test.dart'; + +void main() { + group('Question Serialization', () { + test('JSON serialization is correct for a TextQuestion', () { + final json = { + 'type': 'textQuestion', + 'id': 'q_name', + 'question': 'What is your name?', + 'helpText': 'Please enter your first name', + 'answer': 'Alice', + }; + + final question = Question.fromJson(json) as TextQuestion; + + expect(question.id, 'q_name'); + expect(question.body, 'What is your name?'); + expect(question.helpText, 'Please enter your first name'); + expect(question.answer, 'Alice'); + + final serialized = question.toJson(); + expect(serialized['type'], 'textQuestion'); + expect(serialized['id'], 'q_name'); + expect(serialized['question'], 'What is your name?'); + expect(serialized['helpText'], 'Please enter your first name'); + expect(serialized['answer'], 'Alice'); + }); + + test('JSON serialization is correct for a MultipleChoiceQuestion', () { + final json = { + 'type': 'multipleChoiceQuestion', + 'id': 'q_milk', + 'question': 'What milk do you want?', + 'acceptableAnswers': ['Oat', 'Almond', 'Whole'], + 'selectedValue': 'Oat', + }; + + final question = Question.fromJson(json) as MultipleChoiceQuestion; + + expect(question.id, 'q_milk'); + expect(question.body, 'What milk do you want?'); + expect(question.acceptableAnswers, ['Oat', 'Almond', 'Whole']); + expect(question.selectedValue, 'Oat'); + + final serialized = question.toJson(); + expect(serialized['type'], 'multipleChoiceQuestion'); + expect(serialized['question'], 'What milk do you want?'); + expect(serialized['acceptableAnswers'], ['Oat', 'Almond', 'Whole']); + expect(serialized['selectedValue'], 'Oat'); + }); + + test('JSON serialization is correct for a ZeroToOneQuestion', () { + final json = { + 'type': 'zeroToOneQuestion', + 'id': 'q_roast', + 'question': 'How dark do you want your roast?', + 'minValueLabel': 'Light', + 'maxValueLabel': 'Dark', + 'selectedValue': 0.75, + }; + + final question = Question.fromJson(json) as ZeroToOneQuestion; + + expect(question.id, 'q_roast'); + expect(question.body, 'How dark do you want your roast?'); + expect(question.minValueLabel, 'Light'); + expect(question.maxValueLabel, 'Dark'); + expect(question.selectedValue, 0.75); + + final serialized = question.toJson(); + expect(serialized['type'], 'zeroToOneQuestion'); + expect(serialized['question'], 'How dark do you want your roast?'); + expect(serialized['selectedValue'], 0.75); + }); + + test('JSON serialization is correct for a NegativeOneToOneQuestion', () { + final json = { + 'type': 'negativeOneToOneQuestion', + 'id': 'q_brightness', + 'question': 'Adjust brightness', + 'minValueLabel': 'Darker', + 'maxValueLabel': 'Brighter', + 'selectedValue': -0.5, + }; + + final question = Question.fromJson(json) as NegativeOneToOneQuestion; + + expect(question.id, 'q_brightness'); + expect(question.body, 'Adjust brightness'); + expect(question.minValueLabel, 'Darker'); + expect(question.maxValueLabel, 'Brighter'); + expect(question.selectedValue, -0.5); + + final serialized = question.toJson(); + expect(serialized['type'], 'negativeOneToOneQuestion'); + expect(serialized['question'], 'Adjust brightness'); + expect(serialized['selectedValue'], -0.5); + }); + }); +} diff --git a/genlatte/genlatte-ui/.fvmrc b/genlatte/genlatte-ui/.fvmrc new file mode 100644 index 0000000..09ade4c --- /dev/null +++ b/genlatte/genlatte-ui/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.43.0-0.3.pre" +} \ No newline at end of file diff --git a/genlatte/genlatte-ui/.gitignore b/genlatte/genlatte-ui/.gitignore new file mode 100644 index 0000000..75a6258 --- /dev/null +++ b/genlatte/genlatte-ui/.gitignore @@ -0,0 +1,50 @@ +# 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 + +# FVM Version Cache +.fvm/** + +.agents/skills/ \ No newline at end of file diff --git a/genlatte/genlatte-ui/.metadata b/genlatte/genlatte-ui/.metadata new file mode 100644 index 0000000..112be7c --- /dev/null +++ b/genlatte/genlatte-ui/.metadata @@ -0,0 +1,30 @@ +# 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: "aa1d2edd012bbf31e58827bf3ac7010c10991e20" + channel: "beta" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: aa1d2edd012bbf31e58827bf3ac7010c10991e20 + base_revision: aa1d2edd012bbf31e58827bf3ac7010c10991e20 + - platform: android + create_revision: aa1d2edd012bbf31e58827bf3ac7010c10991e20 + base_revision: aa1d2edd012bbf31e58827bf3ac7010c10991e20 + + # 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' diff --git a/genlatte/genlatte-ui/CONTEXT.md b/genlatte/genlatte-ui/CONTEXT.md new file mode 100644 index 0000000..116b415 --- /dev/null +++ b/genlatte/genlatte-ui/CONTEXT.md @@ -0,0 +1,44 @@ +# GenLatte UI Package (`genlatte-ui/`) + +## Purpose +This directory contains the core Flutter package for the GenLatte project. It provides the design system, shared widgets, and reusable UI logic used across the Kiosk, Barista, and Moderator interfaces. + +## Detailed File Overviews + +- **`pubspec.yaml`**: + - **Description**: The Flutter manifest for the UI package. + - **Key Dependencies**: + - `shadcn_flutter`: The foundational UI component library. + - `flutter_bloc`: For standardized state management throughout the application. + - `google_fonts`: For typography (specifically Google Sans). + +- **`lib/src/`**: + - **Description**: Contains the private implementation of the package. + - **Modules**: + - **`core/`**: Infrastructure including routing, constants, and global state management. + - **`widgets/`**: Reusable design system primitives like `GenLatteScaffold` and `ResponsiveSizedBox`. + - **`screens/`**: UI implementation for specific features (Kiosk home, Recent Orders, etc.). + +- **`widgetbook/`**: + - **Description**: A documentation and testing harness for the package's widgets (see [widgetbook/CONTEXT.md](widgetbook/CONTEXT.md)). + +- **`scripts/`**: + - **Description**: Convenience scripts and symbolic links for development automation (see [scripts/CONTEXT.md](scripts/CONTEXT.md)). + +## Dependencies/Relationships + +- **Backend (Firebase)**: Interacts with Firebase via the source and data layers defined in `lib/src/sources` and `lib/src/data`. +- **Responsive Layout**: Heavily relies on `LayoutProvider` and `DesignScalar` to ensure a consistent experience across desktop, tablet, and mobile orientations. + +## Usage/Exports + +### Package Integration +This package is intended to be imported by consumer Flutter applications: +```yaml +dependencies: + genlatte_ui: + path: ./genlatte-ui +``` + +### UI Standards +Developers should follow the guidelines in the [ui-standard](.agents/skills/ui-standard/SKILL.md) skill to maintain design consistency when adding new components or screens. diff --git a/genlatte/genlatte-ui/GoogleService-Info.plist b/genlatte/genlatte-ui/GoogleService-Info.plist new file mode 100644 index 0000000..04df032 --- /dev/null +++ b/genlatte/genlatte-ui/GoogleService-Info.plist @@ -0,0 +1,32 @@ + + + + + API_KEY + AIzaSyA6AlJnu87HVk3LtCtxBtk8hsyNJEJO_9I + GCM_SENDER_ID + 177381243786 + PLIST_VERSION + 1 + BUNDLE_ID + com.google.genlatte + PROJECT_ID + gcdemos-26-int-dd-latteart + STORAGE_BUCKET + gcdemos-26-int-dd-latteart.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:177381243786:ios:b46a45bfd7ecca11f6e673 + DATABASE_URL + https://gcdemos-26-int-dd-latteart-default-rtdb.firebaseio.com + + \ No newline at end of file diff --git a/genlatte/genlatte-ui/Makefile b/genlatte/genlatte-ui/Makefile new file mode 100644 index 0000000..d8c2955 --- /dev/null +++ b/genlatte/genlatte-ui/Makefile @@ -0,0 +1,42 @@ +app: + fvm flutter run -d web-server --web-port 8080 + +app/kiosk: app + +app/kiosk/wasm: + fvm flutter run -d web-server --wasm --web-port 8080 + +app/barista: + fvm flutter run -d web-server --web-port 8083 + +app/moderator: + fvm flutter run -d web-server --web-port 8085 + +app/queue: + fvm flutter run -d web-server --web-port 8084 + +app/queue/wasm: + fvm flutter run -d web-server --wasm --web-port 8084 + +app/recent: + fvm flutter run -d web-server --web-port 8082 + +app/recent/wasm: + fvm flutter run -d web-server --wasm --web-port 8082 + +gen/ui: + fvm dart run build_runner build --delete-conflicting-outputs + +gen/data: + cd ../genlatte-data && fvm dart run build_runner build --delete-conflicting-outputs + +gen: + $(MAKE) gen/data + $(MAKE) gen/ui + +gen/widgetbook: + cd widgetbook && fvm dart run build_runner build -d + +.PHONY: widgetbook +widgetbook: + cd widgetbook && fvm flutter run -d web-server --web-port 8081 \ No newline at end of file diff --git a/genlatte/genlatte-ui/README.md b/genlatte/genlatte-ui/README.md new file mode 100644 index 0000000..174a834 --- /dev/null +++ b/genlatte/genlatte-ui/README.md @@ -0,0 +1,3 @@ +# genlatte + +A new Flutter project. diff --git a/genlatte/genlatte-ui/analysis_options.yaml b/genlatte/genlatte-ui/analysis_options.yaml new file mode 100644 index 0000000..fcf0b22 --- /dev/null +++ b/genlatte/genlatte-ui/analysis_options.yaml @@ -0,0 +1,17 @@ +include: package:very_good_analysis/analysis_options.yaml +linter: + rules: + public_member_api_docs: true + omit_local_variable_types: false + document_ignores: true + +analyzer: + errors: + todo: ignore + document_ignores: ignore + exclude: + - "**.freezed.dart" + - "**.g.dart" + +formatter: + trailing_commas: preserve \ No newline at end of file diff --git a/genlatte/genlatte-ui/assets/CONTEXT.md b/genlatte/genlatte-ui/assets/CONTEXT.md new file mode 100644 index 0000000..3bdd35b --- /dev/null +++ b/genlatte/genlatte-ui/assets/CONTEXT.md @@ -0,0 +1,12 @@ +# Static Asset Root + +**Purpose:** +The primary container for all binary and configuration assets bundled with the Flutter application. + +**Subdirectories:** +- `videos/`: High-resolution loading and background animations. +- `shaders/`: GLSL fragment shaders for advanced UI effects. +- `avatars/`: Pre-rendered persona images for user identification. + +**Dependencies/Relationships:** +- Assets are declared in `genlatte-ui/pubspec.yaml` to be included in the application bundle. diff --git a/genlatte/genlatte-ui/assets/avatars/CONTEXT.md b/genlatte/genlatte-ui/assets/avatars/CONTEXT.md new file mode 100644 index 0000000..1b11578 --- /dev/null +++ b/genlatte/genlatte-ui/assets/avatars/CONTEXT.md @@ -0,0 +1,10 @@ +# User Avatar Assets + +**Purpose:** +Provides a set of diverse, pre-rendered avatar images used for barista and customer profiles. + +**Detailed File Overviews:** +- `asianFemale.png`, `asianMale.png`, `blackFemale.png`, etc.: High-quality PNG assets representing different personas to provide a localized and inclusive user experience. + +**Dependencies/Relationships:** +- Displayed in the `Kiosk`, `Barista`, and `Moderator` views. diff --git a/genlatte/genlatte-ui/assets/avatars/asianFemale.png b/genlatte/genlatte-ui/assets/avatars/asianFemale.png new file mode 100644 index 0000000..6e08c65 Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/asianFemale.png differ diff --git a/genlatte/genlatte-ui/assets/avatars/asianMale.png b/genlatte/genlatte-ui/assets/avatars/asianMale.png new file mode 100644 index 0000000..2e30891 Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/asianMale.png differ diff --git a/genlatte/genlatte-ui/assets/avatars/blackFemale.png b/genlatte/genlatte-ui/assets/avatars/blackFemale.png new file mode 100644 index 0000000..218409d Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/blackFemale.png differ diff --git a/genlatte/genlatte-ui/assets/avatars/blackMale.png b/genlatte/genlatte-ui/assets/avatars/blackMale.png new file mode 100644 index 0000000..28e4b15 Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/blackMale.png differ diff --git a/genlatte/genlatte-ui/assets/avatars/caucasianFemale.png b/genlatte/genlatte-ui/assets/avatars/caucasianFemale.png new file mode 100644 index 0000000..4d73f6b Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/caucasianFemale.png differ diff --git a/genlatte/genlatte-ui/assets/avatars/caucasianMale.png b/genlatte/genlatte-ui/assets/avatars/caucasianMale.png new file mode 100644 index 0000000..29fcf5d Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/caucasianMale.png differ diff --git a/genlatte/genlatte-ui/assets/avatars/hispanicFemale.png b/genlatte/genlatte-ui/assets/avatars/hispanicFemale.png new file mode 100644 index 0000000..abf32d1 Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/hispanicFemale.png differ diff --git a/genlatte/genlatte-ui/assets/avatars/hispanicMale.png b/genlatte/genlatte-ui/assets/avatars/hispanicMale.png new file mode 100644 index 0000000..e39158f Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/hispanicMale.png differ diff --git a/genlatte/genlatte-ui/assets/avatars/indianFemale.png b/genlatte/genlatte-ui/assets/avatars/indianFemale.png new file mode 100644 index 0000000..c08b2f6 Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/indianFemale.png differ diff --git a/genlatte/genlatte-ui/assets/avatars/indianMale.png b/genlatte/genlatte-ui/assets/avatars/indianMale.png new file mode 100644 index 0000000..cc023b4 Binary files /dev/null and b/genlatte/genlatte-ui/assets/avatars/indianMale.png differ diff --git a/genlatte/genlatte-ui/assets/firebase-logo.png b/genlatte/genlatte-ui/assets/firebase-logo.png new file mode 100644 index 0000000..5524d8d Binary files /dev/null and b/genlatte/genlatte-ui/assets/firebase-logo.png differ diff --git a/genlatte/genlatte-ui/assets/flutter-latte.png b/genlatte/genlatte-ui/assets/flutter-latte.png new file mode 100644 index 0000000..d28f9aa Binary files /dev/null and b/genlatte/genlatte-ui/assets/flutter-latte.png differ diff --git a/genlatte/genlatte-ui/assets/flutter-logo.png b/genlatte/genlatte-ui/assets/flutter-logo.png new file mode 100644 index 0000000..4d545bd Binary files /dev/null and b/genlatte/genlatte-ui/assets/flutter-logo.png differ diff --git a/genlatte/genlatte-ui/assets/gemini-logo.png b/genlatte/genlatte-ui/assets/gemini-logo.png new file mode 100644 index 0000000..6126770 Binary files /dev/null and b/genlatte/genlatte-ui/assets/gemini-logo.png differ diff --git a/genlatte/genlatte-ui/assets/green-check.svg b/genlatte/genlatte-ui/assets/green-check.svg new file mode 100644 index 0000000..f82dffb --- /dev/null +++ b/genlatte/genlatte-ui/assets/green-check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/genlatte/genlatte-ui/assets/io-primary-gradient-mesh-small.jpg b/genlatte/genlatte-ui/assets/io-primary-gradient-mesh-small.jpg new file mode 100644 index 0000000..1d4913c Binary files /dev/null and b/genlatte/genlatte-ui/assets/io-primary-gradient-mesh-small.jpg differ diff --git a/genlatte/genlatte-ui/assets/latte-background-thumb.png b/genlatte/genlatte-ui/assets/latte-background-thumb.png new file mode 100644 index 0000000..0dc7510 Binary files /dev/null and b/genlatte/genlatte-ui/assets/latte-background-thumb.png differ diff --git a/genlatte/genlatte-ui/assets/latte-background.png b/genlatte/genlatte-ui/assets/latte-background.png new file mode 100644 index 0000000..77cef4c Binary files /dev/null and b/genlatte/genlatte-ui/assets/latte-background.png differ diff --git a/genlatte/genlatte-ui/assets/shaders/CONTEXT.md b/genlatte/genlatte-ui/assets/shaders/CONTEXT.md new file mode 100644 index 0000000..9e6954e --- /dev/null +++ b/genlatte/genlatte-ui/assets/shaders/CONTEXT.md @@ -0,0 +1,11 @@ +# Custom GPU Shaders + +**Purpose:** +Contains GLSL fragment shaders utilized by the Flutter engine for high-performance visual effects. + +**Detailed File Overviews:** +- `squish.glsl`: A custom shader used to implement the physics-based "squish" effect on latte images, allowing for realistic deformations during collisions or interactions. + +**Dependencies/Relationships:** +- Integrated into the Flutter build process via the `pubspec.yaml` shader definitions. +- Utilized by the `SquishableLatteImage` widget. diff --git a/genlatte/genlatte-ui/assets/shaders/squish.glsl b/genlatte/genlatte-ui/assets/shaders/squish.glsl new file mode 100644 index 0000000..40cda3f --- /dev/null +++ b/genlatte/genlatte-ui/assets/shaders/squish.glsl @@ -0,0 +1,59 @@ +#version 460 core + +precision mediump float; + +#include + +uniform vec2 uSize; +uniform float uAngle; +uniform float uSquishFactor; + +uniform sampler2D tTexture; + +out vec4 fragColor; + +void fragment(vec2 uv, vec2 pos, inout vec4 color) { + + vec2 transformedUV = uv.xy; + + transformedUV -= 0.5; + + // float angle = radians(uSquishAngleOrigin); + + float cosAngle = cos(uAngle); + float sinAngle = sin(uAngle); + mat2 rotationMatrix = mat2(cosAngle, -sinAngle, sinAngle, cosAngle); + + transformedUV = rotationMatrix * transformedUV; + + // Utilizing the original proportional logic but shifting uSquishFactor so that 0 is the break-even point. + // A positive effect strength maps to a smaller squishFactor. + float squishFactor = 1.0 - uSquishFactor; + float halfSquishFactor = ((squishFactor - 1.0) * 0.5) + 1.0; + + // Squish the axes using the original logic ratio + transformedUV.y *= squishFactor; + transformedUV.x *= (1.0 / halfSquishFactor); + + mat2 inverseRotationMatrix = mat2(cosAngle, sinAngle, -sinAngle, cosAngle); + transformedUV = inverseRotationMatrix * transformedUV; + + transformedUV += 0.5; + + if(transformedUV.x < 0.0 || transformedUV.x > 1.0 || transformedUV.y < 0.0 || transformedUV.y > 1.0) { + color = vec4(0.0, 0.0, 0.0, 0.0); + return; + } + + color = texture(tTexture, transformedUV); +} + +void main() { + vec2 pos = FlutterFragCoord().xy; + vec2 uv = pos / uSize; + vec4 color; + + fragment(uv, pos, color); + + fragColor = color; +} \ No newline at end of file diff --git a/genlatte/genlatte-ui/assets/videos/CONTEXT.md b/genlatte/genlatte-ui/assets/videos/CONTEXT.md new file mode 100644 index 0000000..3fe6574 --- /dev/null +++ b/genlatte/genlatte-ui/assets/videos/CONTEXT.md @@ -0,0 +1,10 @@ +# UI Video Assets + +**Purpose:** +Stores high-resolution video assets used for loading screens and background animations in the Flutter application. + +**Detailed File Overviews:** +- `Dash_Loading_1080px.webm`: An optimized video file featuring the Dash mascot, used during long-running async operations or initial application load. + +**Dependencies/Relationships:** +- Loaded by video player widgets in `genlatte-ui/lib/src/widgets/`. diff --git a/genlatte/genlatte-ui/assets/videos/Dash_Loading_1080px.webm b/genlatte/genlatte-ui/assets/videos/Dash_Loading_1080px.webm new file mode 100644 index 0000000..299b6da Binary files /dev/null and b/genlatte/genlatte-ui/assets/videos/Dash_Loading_1080px.webm differ diff --git a/genlatte/genlatte-ui/assets/videos/Dash_Loading_1080px_H.265.mov b/genlatte/genlatte-ui/assets/videos/Dash_Loading_1080px_H.265.mov new file mode 100644 index 0000000..8b9febd Binary files /dev/null and b/genlatte/genlatte-ui/assets/videos/Dash_Loading_1080px_H.265.mov differ diff --git a/genlatte/genlatte-ui/devtools_options.yaml b/genlatte/genlatte-ui/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/genlatte/genlatte-ui/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/genlatte/genlatte-ui/firebase.json b/genlatte/genlatte-ui/firebase.json new file mode 100644 index 0000000..9d40c9e --- /dev/null +++ b/genlatte/genlatte-ui/firebase.json @@ -0,0 +1,14 @@ +{ + "flutter": { + "platforms": { + "dart": { + "lib/firebase_options.dart": { + "projectId": "gcdemos-26-int-dd-latteart", + "configurations": { + "web": "1:177381243786:web:dea75ce782190ef7f6e673" + } + } + } + } + } +} \ No newline at end of file diff --git a/genlatte/genlatte-ui/lib/CONTEXT.md b/genlatte/genlatte-ui/lib/CONTEXT.md new file mode 100644 index 0000000..de5840a --- /dev/null +++ b/genlatte/genlatte-ui/lib/CONTEXT.md @@ -0,0 +1,15 @@ +# Application Library Root + +**Purpose:** +The primary source code root for the Flutter application. + +**Detailed File Overviews:** +- `firebase_options.dart`: Autogenerated Firebase configuration for all supported platforms (Web, iOS, etc.). +- `main.dart`: The main entry point that executes the `bootstrap` process. + +**Subdirectories:** +- `src/`: Encompasses the core logic, UI screens, and features of the app. +- `hive/`: Configuration for local data persistence. + +**Dependencies/Relationships:** +- Entry point for the entire presentation layer. diff --git a/genlatte/genlatte-ui/lib/firebase_options.dart b/genlatte/genlatte-ui/lib/firebase_options.dart new file mode 100644 index 0000000..96c1732 --- /dev/null +++ b/genlatte/genlatte-ui/lib/firebase_options.dart @@ -0,0 +1,88 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyA0mu0fBBQs2lNVCW861kkDTQSNcmBQjP4', + appId: '1:177381243786:web:dea75ce782190ef7f6e673', + messagingSenderId: '177381243786', + projectId: 'gcdemos-26-int-dd-latteart', + authDomain: 'gcdemos-26-int-dd-latteart.firebaseapp.com', + databaseURL: + 'https://gcdemos-26-int-dd-latteart-default-rtdb.firebaseio.com', + storageBucket: 'gcdemos-26-int-dd-latteart.firebasestorage.app', + measurementId: "G-KHJ9QF44VC", + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyA6AlJnu87HVk3LtCtxBtk8hsyNJEJO_9I', + appId: '1:177381243786:ios:b46a45bfd7ecca11f6e673', + messagingSenderId: '177381243786', + projectId: 'gcdemos-26-int-dd-latteart', + authDomain: 'gcdemos-26-int-dd-latteart.firebaseapp.com', + databaseURL: + 'https://gcdemos-26-int-dd-latteart-default-rtdb.firebaseio.com', + storageBucket: 'gcdemos-26-int-dd-latteart.firebasestorage.app', + measurementId: "G-KHJ9QF44VC", + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAhqEHefNYgZ_GgKnnqhieD7O_qDvclbEI', + appId: '1:177381243786:android:38afc480de87d7b2f6e673', + messagingSenderId: '177381243786', + projectId: 'gcdemos-26-int-dd-latteart', + authDomain: 'gcdemos-26-int-dd-latteart.firebaseapp.com', + databaseURL: + 'https://gcdemos-26-int-dd-latteart-default-rtdb.firebaseio.com', + storageBucket: 'gcdemos-26-int-dd-latteart.firebasestorage.app', + measurementId: "G-KHJ9QF44VC", + ); +} diff --git a/genlatte/genlatte-ui/lib/hive/CONTEXT.md b/genlatte/genlatte-ui/lib/hive/CONTEXT.md new file mode 100644 index 0000000..2a92c49 --- /dev/null +++ b/genlatte/genlatte-ui/lib/hive/CONTEXT.md @@ -0,0 +1,14 @@ +# Local Storage Persistence (Hive) + +**Purpose:** +Configures Hive for local NoSQL persistence, which caches data on the device to improve performance and allow for offline functionality. + +**Detailed File Overviews:** +- `hive_adapters.dart`: Central registration point for Hive TypeAdapters. It specifies which domain models (from `genlatte-data`) should be serialized by Hive. +- `hive_adapters.g.dart`: Autogenerated serialization logic. +- `hive_registrar.g.dart`: Autogenerated registration boilerplate. +- `AppHiveInitializer`: Coordinates the `Hive.initFlutter()` process and signals when local storage is ready for use. + +**Dependencies/Relationships:** +- Bridges `genlatte-data` models with the `hive_ce` storage engine. +- Used by the `HiveSources` in the data layer. diff --git a/genlatte/genlatte-ui/lib/hive/hive_adapters.dart b/genlatte/genlatte-ui/lib/hive/hive_adapters.dart new file mode 100644 index 0000000..0c2d5d5 --- /dev/null +++ b/genlatte/genlatte-ui/lib/hive/hive_adapters.dart @@ -0,0 +1,48 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:data_layer_hive/data_layer_hive.dart'; +import 'package:genlatte/hive/hive_registrar.g.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; +import 'package:logging/logging.dart'; + +@GenerateAdapters([ + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), +]) +part 'hive_adapters.g.dart'; + +final _log = Logger('HiveInitializer'); + +/// {@template AppHiveInitializer} +/// Class which exists to call `Hive.initFlutter` and `Hive.registerAdapters` +/// and report back when both are complete. +/// {@endtemplate} +class AppHiveInitializer extends HiveInitializer { + /// {@macro AppHiveInitializer} + AppHiveInitializer() { + initialize(); + } + + @override + Future performInitialization() { + return Hive.initFlutter().then((_) { + try { + Hive.registerAdapters(); + markReady(null); + } on Exception catch (e, st) { + _log.shout('Failed to initialize Hive: $e :: $st'); + } + }); + } +} diff --git a/genlatte/genlatte-ui/lib/hive/hive_adapters.g.dart b/genlatte/genlatte-ui/lib/hive/hive_adapters.g.dart new file mode 100644 index 0000000..c658a80 --- /dev/null +++ b/genlatte/genlatte-ui/lib/hive/hive_adapters.g.dart @@ -0,0 +1,503 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'hive_adapters.dart'; + +// ************************************************************************** +// AdaptersGenerator +// ************************************************************************** + +class LatteOrderAdapter extends TypeAdapter { + @override + final typeId = 0; + + @override + LatteOrder read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return LatteOrder( + id: fields[0] as String?, + name: fields[1] as String?, + milk: fields[2] as String?, + sweetener: fields[3] as String?, + happyPlace: fields[4] as String?, + ); + } + + @override + void write(BinaryWriter writer, LatteOrder obj) { + writer + ..writeByte(5) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.milk) + ..writeByte(3) + ..write(obj.sweetener) + ..writeByte(4) + ..write(obj.happyPlace); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LatteOrderAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class LatteOrderMetadataAdapter extends TypeAdapter { + @override + final typeId = 1; + + @override + LatteOrderMetadata read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return LatteOrderMetadata( + id: fields[0] as String?, + orderNumber: (fields[1] as num?)?.toInt(), + isNameApproved: fields[2] as bool?, + isHappyPlaceApproved: fields[3] as bool?, + happyPlaceModerationReason: fields[4] as String?, + isImageApproved: fields[5] as bool?, + imageBatchId: fields[6] as String?, + imageUrl: fields[7] as String?, + status: fields[8] == null + ? LatteOrderStatus.configuring + : fields[8] as LatteOrderStatus, + baristaId: fields[11] as String?, + orderSubmittedTime: fields[9] as DateTime?, + completionTime: fields[10] as DateTime?, + ); + } + + @override + void write(BinaryWriter writer, LatteOrderMetadata obj) { + writer + ..writeByte(12) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.orderNumber) + ..writeByte(2) + ..write(obj.isNameApproved) + ..writeByte(3) + ..write(obj.isHappyPlaceApproved) + ..writeByte(4) + ..write(obj.happyPlaceModerationReason) + ..writeByte(5) + ..write(obj.isImageApproved) + ..writeByte(6) + ..write(obj.imageBatchId) + ..writeByte(7) + ..write(obj.imageUrl) + ..writeByte(8) + ..write(obj.status) + ..writeByte(9) + ..write(obj.orderSubmittedTime) + ..writeByte(10) + ..write(obj.completionTime) + ..writeByte(11) + ..write(obj.baristaId); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LatteOrderMetadataAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class LatteOrderStatusAdapter extends TypeAdapter { + @override + final typeId = 2; + + @override + LatteOrderStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return LatteOrderStatus.configuring; + case 1: + return LatteOrderStatus.submitted; + case 2: + return LatteOrderStatus.validated; + case 3: + return LatteOrderStatus.inProgress; + case 4: + return LatteOrderStatus.completed; + case 5: + return LatteOrderStatus.archived; + default: + return LatteOrderStatus.configuring; + } + } + + @override + void write(BinaryWriter writer, LatteOrderStatus obj) { + switch (obj) { + case LatteOrderStatus.configuring: + writer.writeByte(0); + case LatteOrderStatus.submitted: + writer.writeByte(1); + case LatteOrderStatus.validated: + writer.writeByte(2); + case LatteOrderStatus.inProgress: + writer.writeByte(3); + case LatteOrderStatus.completed: + writer.writeByte(4); + case LatteOrderStatus.archived: + writer.writeByte(5); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LatteOrderStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class LatteImageBatchAdapter extends TypeAdapter { + @override + final typeId = 3; + + @override + LatteImageBatch read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return LatteImageBatch( + id: fields[0] as String, + orderId: fields[1] as String, + image0: fields[5] as LatteImage?, + image1: fields[2] as LatteImage?, + image2: fields[3] as LatteImage?, + image3: fields[4] as LatteImage?, + parent: fields[6] as LatteImageBatchParent?, + ); + } + + @override + void write(BinaryWriter writer, LatteImageBatch obj) { + writer + ..writeByte(7) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.orderId) + ..writeByte(2) + ..write(obj.image1) + ..writeByte(3) + ..write(obj.image2) + ..writeByte(4) + ..write(obj.image3) + ..writeByte(5) + ..write(obj.image0) + ..writeByte(6) + ..write(obj.parent); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LatteImageBatchAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class LatteImageAdapter extends TypeAdapter { + @override + final typeId = 4; + + @override + LatteImage read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return LatteImage( + imageUrl: fields[0] as String, + prompt: fields[1] as String, + questions: (fields[2] as List?)?.cast(), + description: fields[3] as String, + ); + } + + @override + void write(BinaryWriter writer, LatteImage obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.imageUrl) + ..writeByte(1) + ..write(obj.prompt) + ..writeByte(2) + ..write(obj.questions) + ..writeByte(3) + ..write(obj.description); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LatteImageAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class QuestionAdapter extends TypeAdapter { + @override + final typeId = 5; + + @override + Question read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Question( + id: fields[0] as String, + body: fields[1] as String, + answer: fields[2] as String?, + ); + } + + @override + void write(BinaryWriter writer, Question obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.body) + ..writeByte(2) + ..write(obj.answer); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QuestionAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class BaristaAdapter extends TypeAdapter { + @override + final typeId = 6; + + @override + Barista read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Barista( + username: fields[0] as String, + persona: fields[1] as BaristaPersona, + id: fields[2] as String?, + ); + } + + @override + void write(BinaryWriter writer, Barista obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.username) + ..writeByte(1) + ..write(obj.persona) + ..writeByte(2) + ..write(obj.id); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BaristaAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class LatteAdapter extends TypeAdapter { + @override + final typeId = 7; + + @override + Latte read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Latte( + order: fields[0] as LatteOrder, + metadata: fields[1] as LatteOrderMetadata, + ); + } + + @override + void write(BinaryWriter writer, Latte obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.order) + ..writeByte(1) + ..write(obj.metadata); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LatteAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class BaristaPersonaAdapter extends TypeAdapter { + @override + final typeId = 8; + + @override + BaristaPersona read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return BaristaPersona.blackFemale; + case 1: + return BaristaPersona.asianFemale; + case 2: + return BaristaPersona.caucasianFemale; + case 3: + return BaristaPersona.hispanicFemale; + case 4: + return BaristaPersona.indianFemale; + case 5: + return BaristaPersona.blackMale; + case 6: + return BaristaPersona.asianMale; + case 7: + return BaristaPersona.caucasianMale; + case 8: + return BaristaPersona.hispanicMale; + case 9: + return BaristaPersona.indianMale; + default: + return BaristaPersona.blackFemale; + } + } + + @override + void write(BinaryWriter writer, BaristaPersona obj) { + switch (obj) { + case BaristaPersona.blackFemale: + writer.writeByte(0); + case BaristaPersona.asianFemale: + writer.writeByte(1); + case BaristaPersona.caucasianFemale: + writer.writeByte(2); + case BaristaPersona.hispanicFemale: + writer.writeByte(3); + case BaristaPersona.indianFemale: + writer.writeByte(4); + case BaristaPersona.blackMale: + writer.writeByte(5); + case BaristaPersona.asianMale: + writer.writeByte(6); + case BaristaPersona.caucasianMale: + writer.writeByte(7); + case BaristaPersona.hispanicMale: + writer.writeByte(8); + case BaristaPersona.indianMale: + writer.writeByte(9); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BaristaPersonaAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class MachineAdapter extends TypeAdapter { + @override + final typeId = 9; + + @override + Machine read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Machine( + id: fields[0] as String, + name: fields[1] as String, + isActive: fields[3] == null ? true : fields[3] as bool, + isBlackAndWhite: fields[2] == null ? true : fields[2] as bool, + ); + } + + @override + void write(BinaryWriter writer, Machine obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.isBlackAndWhite) + ..writeByte(3) + ..write(obj.isActive); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MachineAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/genlatte/genlatte-ui/lib/hive/hive_adapters.g.yaml b/genlatte/genlatte-ui/lib/hive/hive_adapters.g.yaml new file mode 100644 index 0000000..155fa43 --- /dev/null +++ b/genlatte/genlatte-ui/lib/hive/hive_adapters.g.yaml @@ -0,0 +1,157 @@ +# Generated by Hive CE +# Manual modifications may be necessary for certain migrations +# Check in to version control +nextTypeId: 10 +types: + LatteOrder: + typeId: 0 + nextIndex: 5 + fields: + id: + index: 0 + name: + index: 1 + milk: + index: 2 + sweetener: + index: 3 + happyPlace: + index: 4 + LatteOrderMetadata: + typeId: 1 + nextIndex: 12 + fields: + id: + index: 0 + orderNumber: + index: 1 + isNameApproved: + index: 2 + isHappyPlaceApproved: + index: 3 + happyPlaceModerationReason: + index: 4 + isImageApproved: + index: 5 + imageBatchId: + index: 6 + imageUrl: + index: 7 + status: + index: 8 + orderSubmittedTime: + index: 9 + completionTime: + index: 10 + baristaId: + index: 11 + LatteOrderStatus: + typeId: 2 + nextIndex: 6 + fields: + configuring: + index: 0 + submitted: + index: 1 + validated: + index: 2 + inProgress: + index: 3 + completed: + index: 4 + archived: + index: 5 + LatteImageBatch: + typeId: 3 + nextIndex: 7 + fields: + id: + index: 0 + orderId: + index: 1 + image1: + index: 2 + image2: + index: 3 + image3: + index: 4 + image0: + index: 5 + parent: + index: 6 + LatteImage: + typeId: 4 + nextIndex: 4 + fields: + imageUrl: + index: 0 + prompt: + index: 1 + questions: + index: 2 + description: + index: 3 + Question: + typeId: 5 + nextIndex: 3 + fields: + id: + index: 0 + body: + index: 1 + answer: + index: 2 + Barista: + typeId: 6 + nextIndex: 3 + fields: + username: + index: 0 + persona: + index: 1 + id: + index: 2 + Latte: + typeId: 7 + nextIndex: 2 + fields: + order: + index: 0 + metadata: + index: 1 + BaristaPersona: + typeId: 8 + nextIndex: 10 + fields: + blackFemale: + index: 0 + asianFemale: + index: 1 + caucasianFemale: + index: 2 + hispanicFemale: + index: 3 + indianFemale: + index: 4 + blackMale: + index: 5 + asianMale: + index: 6 + caucasianMale: + index: 7 + hispanicMale: + index: 8 + indianMale: + index: 9 + Machine: + typeId: 9 + nextIndex: 4 + fields: + id: + index: 0 + name: + index: 1 + isBlackAndWhite: + index: 2 + isActive: + index: 3 diff --git a/genlatte/genlatte-ui/lib/hive/hive_registrar.g.dart b/genlatte/genlatte-ui/lib/hive/hive_registrar.g.dart new file mode 100644 index 0000000..7956e10 --- /dev/null +++ b/genlatte/genlatte-ui/lib/hive/hive_registrar.g.dart @@ -0,0 +1,40 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Generated by Hive CE +// Do not modify +// Check in to version control + +import 'package:hive_ce/hive_ce.dart'; +import 'package:genlatte/hive/hive_adapters.dart'; + +extension HiveRegistrar on HiveInterface { + void registerAdapters() { + registerAdapter(BaristaAdapter()); + registerAdapter(BaristaPersonaAdapter()); + registerAdapter(LatteAdapter()); + registerAdapter(LatteImageAdapter()); + registerAdapter(LatteImageBatchAdapter()); + registerAdapter(LatteOrderAdapter()); + registerAdapter(LatteOrderMetadataAdapter()); + registerAdapter(LatteOrderStatusAdapter()); + registerAdapter(MachineAdapter()); + registerAdapter(QuestionAdapter()); + } +} + +extension IsolatedHiveRegistrar on IsolatedHiveInterface { + void registerAdapters() { + registerAdapter(BaristaAdapter()); + registerAdapter(BaristaPersonaAdapter()); + registerAdapter(LatteAdapter()); + registerAdapter(LatteImageAdapter()); + registerAdapter(LatteImageBatchAdapter()); + registerAdapter(LatteOrderAdapter()); + registerAdapter(LatteOrderMetadataAdapter()); + registerAdapter(LatteOrderStatusAdapter()); + registerAdapter(MachineAdapter()); + registerAdapter(QuestionAdapter()); + } +} diff --git a/genlatte/genlatte-ui/lib/main.dart b/genlatte/genlatte-ui/lib/main.dart new file mode 100644 index 0000000..7dcda97 --- /dev/null +++ b/genlatte/genlatte-ui/lib/main.dart @@ -0,0 +1,12 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Convenience entrypoint to allow `$flutter run` to work during development. +/// During deployment, prefer one of the explicit entrypoints. +library; + +import 'package:genlatte/src/bootstrap.dart'; +import 'package:genlatte/src/core/core.dart'; + +Future main() async => bootstrap(appEnv: AppEnv.current); diff --git a/genlatte/genlatte-ui/lib/main_dev.dart b/genlatte/genlatte-ui/lib/main_dev.dart new file mode 100644 index 0000000..0e858fc --- /dev/null +++ b/genlatte/genlatte-ui/lib/main_dev.dart @@ -0,0 +1,9 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/bootstrap.dart'; +import 'package:genlatte/src/core/core.dart'; + +/// Entrypoint for the development environment. +Future main() async => bootstrap(appEnv: AppEnv.dev); diff --git a/genlatte/genlatte-ui/lib/main_prod.dart b/genlatte/genlatte-ui/lib/main_prod.dart new file mode 100644 index 0000000..dd88e5e --- /dev/null +++ b/genlatte/genlatte-ui/lib/main_prod.dart @@ -0,0 +1,9 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/bootstrap.dart'; +import 'package:genlatte/src/core/core.dart'; + +/// Entrypoint for the production environment. +Future main() async => bootstrap(appEnv: AppEnv.prod); diff --git a/genlatte/genlatte-ui/lib/main_staging.dart b/genlatte/genlatte-ui/lib/main_staging.dart new file mode 100644 index 0000000..826ae78 --- /dev/null +++ b/genlatte/genlatte-ui/lib/main_staging.dart @@ -0,0 +1,9 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/bootstrap.dart'; +import 'package:genlatte/src/core/core.dart'; + +/// Entrypoint for the staging environment. +Future main() async => bootstrap(appEnv: AppEnv.staging); diff --git a/genlatte/genlatte-ui/lib/src/CONTEXT.md b/genlatte/genlatte-ui/lib/src/CONTEXT.md new file mode 100644 index 0000000..e254845 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/CONTEXT.md @@ -0,0 +1,17 @@ +# Application Source Core + +**Purpose:** +The centralized collection of the application's internal implementation details, from bootstrapping to feature-specific logic. + +**Detailed File Overviews:** +- `bootstrap.dart`: Orchestrates the application startup sequence, including Firebase initialization, dependency injection (using GetIt), Hive setup, and URL strategy configuration. +- `role.dart`: Defines the user roles and permissions logic utilized by the Auth and Routing systems. + +**Subdirectories:** +- `core/`: Foundation logic like Auth and Routing. +- `data/` / `sources/`: Data repositories and remote data providers. +- `screens/`: Feature-specific UI trees. +- `widgets/`: Shared UI components and primitives. + +**Dependencies/Relationships:** +- Synthesizes all internal modules into a cohesive application. diff --git a/genlatte/genlatte-ui/lib/src/bootstrap.dart b/genlatte/genlatte-ui/lib/src/bootstrap.dart new file mode 100644 index 0000000..151d003 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/bootstrap.dart @@ -0,0 +1,108 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:data_layer_hive/data_layer_hive.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_web_plugins/url_strategy.dart'; +import 'package:genlatte/firebase_options.dart'; +import 'package:genlatte/src/core/core.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; + +/// {@template bootstrap} +/// Application bootstrap. +/// {@endtemplate} +Future bootstrap({AppEnv? appEnv}) async { + WidgetsFlutterBinding.ensureInitialized(); + usePathUrlStrategy(); + + final env = appEnv ?? AppEnv.current; + + final defaultOptions = DefaultFirebaseOptions.currentPlatform; + final customMeasurementId = env.isProd + ? defaultOptions.measurementId + : 'G-51CFKR9L9R'; + + final options = FirebaseOptions( + apiKey: defaultOptions.apiKey, + appId: defaultOptions.appId, + messagingSenderId: defaultOptions.messagingSenderId, + projectId: defaultOptions.projectId, + authDomain: defaultOptions.authDomain, + databaseURL: defaultOptions.databaseURL, + storageBucket: defaultOptions.storageBucket, + measurementId: customMeasurementId, + ); + + final app = await Firebase.initializeApp( + options: options, + ); + + await setUpDependencyInjection( + appEnv: env, + firebaseFirestore: FirebaseFirestore.instanceFor( + app: app, + databaseId: 'gcdemos-26-int-dd-latteart-${env.name}', + ), + firebaseFunctions: FirebaseFunctions.instance, + firebaseAnalytics: FirebaseAnalytics.instance, + firebaseRemoteConfig: FirebaseRemoteConfig.instance, + ); + + // Register Hive adapaters and signal to HiveSources when that process + // is complete. + GetIt.I().initialize(); + + // await deleteAllDocumentsInCollection('latteOrders'); + // await deleteAllDocumentsInCollection('latteImageBatches'); + // await deleteAllDocumentsInCollection('latteOrderMetadata'); + // await deleteAllDocumentsInCollection('recentLatteImages'); + + await migrateOptionsCollection(); + + Logger.root.level = Level.FINE; // defaults to Level.INFO + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); + }); + + runApp( + AppView( + authBloc: GetIt.I(), + appRouter: GetIt.I(), + ), + ); +} + +/// Helper function to delete all documents in a specified Firestore collection. +Future deleteAllDocumentsInCollection(String collectionName) async { + final firestore = GetIt.I(); + final snapshot = await firestore.collection(collectionName).get(); + await Future.wait(snapshot.docs.map((doc) => doc.reference.delete())); + debugPrint('Deleted ${snapshot.docs.length} documents from $collectionName'); +} + +/// Migrates the latteOptions collection to the new format. No-op once +/// already migrated. This can be deleted after the first application runs this +/// code against production. +Future migrateOptionsCollection() async { + final firestore = GetIt.I(); + final optionsSnapshot = await firestore.collection('latteOptions').get(); + for (final doc in optionsSnapshot.docs) { + final data = doc.data(); + final values = data['values'] as List?; + if (values != null && values.isNotEmpty && values.first is String) { + final newValues = values + .map((v) => {'name': v as String, 'isAvailable': true}) + .toList(); + await doc.reference.update({'values': newValues}); + debugPrint('Migrated LatteOptions document: ${doc.id}'); + } + } +} diff --git a/genlatte/genlatte-ui/lib/src/core/CONTEXT.md b/genlatte/genlatte-ui/lib/src/core/CONTEXT.md new file mode 100644 index 0000000..8de0da9 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/CONTEXT.md @@ -0,0 +1,7 @@ +# Core App Sub-Tree + +**Purpose:** +A structural parent grouping directory that hosts fundamental architectural configurations globally required, but not tied to specific user paths (like Authentication and deep-linked Routing Maps). + +**Implementation Details:** +- **Child Contexts**: Please navigate into `auth/`, `routing/`, or `utils/` to see exact functional breakdowns for Identity management, Navigation interceptors, and Cloud helper logics. diff --git a/genlatte/genlatte-ui/lib/src/core/app_env.dart b/genlatte/genlatte-ui/lib/src/core/app_env.dart new file mode 100644 index 0000000..836fd0f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/app_env.dart @@ -0,0 +1,40 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Represents the supported application environments. +enum AppEnv { + /// Local development environment. + dev, + + /// Shared staging environment. + staging, + + /// Production environment. + prod; + + /// Parses an [AppEnv] from a [String]. + static AppEnv fromString(String? value) { + return switch (value?.toLowerCase()) { + 'dev' => AppEnv.dev, + 'staging' => AppEnv.staging, + 'prod' => AppEnv.prod, + _ => AppEnv.staging, + }; + } + + /// Returns the current [AppEnv] based on the `APP_ENV` environment variable. + static AppEnv get current { + const value = String.fromEnvironment('APP_ENV'); + return fromString(value); + } + + /// Whether the current environment is [AppEnv.dev]. + bool get isDev => this == AppEnv.dev; + + /// Whether the current environment is [AppEnv.staging]. + bool get isStaging => this == AppEnv.staging; + + /// Whether the current environment is [AppEnv.prod]. + bool get isProd => this == AppEnv.prod; +} diff --git a/genlatte/genlatte-ui/lib/src/core/auth/CONTEXT.md b/genlatte/genlatte-ui/lib/src/core/auth/CONTEXT.md new file mode 100644 index 0000000..69f3ba9 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/auth/CONTEXT.md @@ -0,0 +1,17 @@ +# Auth Core + +**Purpose:** +Provides the centralized identity management mechanism for the entire GenLatte UI application. + +**Detailed File Overviews:** + +- `auth.dart`: + - **Description**: Barrel export file. + +- `auth_bloc.dart`: + - **Description**: The BLoC controlling authentication state. + - **Core Logic**: Subscribes to `FirebaseAuth.authStateChanges()`. Upon receiving a new `User`, it forces an ID token refresh to retrieve custom claims directly from Firebase. These claims dictate the user's `Role` (`barista`, `kiosk`, `queue`, `recent`, `moderator`). If a user has no role, it automatically signs them out for security. + +**Dependencies/Relationships:** +- Tightly coupled with the `FirebaseAuth` plugin. +- Direct output is consumed by `core/routing/redirection.dart` to determine allowed URL paths. diff --git a/genlatte/genlatte-ui/lib/src/core/auth/auth.dart b/genlatte/genlatte-ui/lib/src/core/auth/auth.dart new file mode 100644 index 0000000..26bf4b3 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/auth/auth.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'auth_bloc.dart'; diff --git a/genlatte/genlatte-ui/lib/src/core/auth/auth_bloc.dart b/genlatte/genlatte-ui/lib/src/core/auth/auth_bloc.dart new file mode 100644 index 0000000..1111a9c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/auth/auth_bloc.dart @@ -0,0 +1,122 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte/src/role.dart'; +import 'package:logging/logging.dart'; + +part 'auth_bloc.freezed.dart'; + +final _log = Logger('AuthBloc'); + +typedef _Emit = Emitter; + +/// {@template AuthBloc} +/// Provider of application wide authentication state. +/// {@endtemplate} +class AuthBloc extends Bloc { + /// {@macro AuthBloc} + AuthBloc(this._authService) : super(AuthState.initial()) { + on( + (event, _Emit emit) => switch (event) { + NewUserEvent() => _onNewUserEvent(event, emit), + LoggedOutEvent() => _onLoggedOutEvent(event, emit), + }, + ); + + // Listen to authentication changes + _authSub = _authService.authStateChanges().listen(_onAuthChanges); + } + final FirebaseAuth _authService; + late final StreamSubscription _authSub; + + Future _onAuthChanges(User? user) async { + if (user != null) { + final role = await getCurrentUserClaims(user); + if (role == null) { + _log.warning('Signing out User ${user.uid} for having no role'); + await _authService.signOut(); + add(const LoggedOutEvent()); + return; + } + add(NewUserEvent(user, role)); + } else { + add(const LoggedOutEvent()); + } + } + + /// Fetches the user's current claims from Firebase. + Future getCurrentUserClaims(User user) async { + try { + // Force a token refresh to ensure claims are current + final IdTokenResult idTokenResult = await user.getIdTokenResult(); + + // Access the claims map + final Map? claims = idTokenResult.claims; + + if (claims == null) { + _log.severe('User ${user.uid} logged in without claims.'); + return null; + } + + if (claims['barista'] == true) { + return .barista; + } else if (claims['kiosk'] == true) { + return .kiosk; + } else if (claims['queue'] == true) { + return .queueObserver; + } else if (claims['recent'] == true) { + return .recentOrdersObserver; + } else if (claims['moderator'] == true) { + return .moderator; + } + + _log.severe('User ${user.uid} logged in with invalid role :: $claims'); + return null; + } on FirebaseAuthException catch (e) { + _log.severe('Error fetching ID token result: ${e.message}'); + return null; + } + } + + void _onNewUserEvent(NewUserEvent event, _Emit emit) => + emit(state.copyWith(user: event.user, role: event.role)); + + Future _onLoggedOutEvent(LoggedOutEvent event, _Emit emit) async { + emit(state.copyWith(user: null, role: null)); + } + + @override + Future close() async { + unawaited(_authSub.cancel()); + await super.close(); + } +} + +/// Actions that can be taken on the auth page. +@Freezed() +sealed class AuthEvent with _$AuthEvent { + const factory AuthEvent.newUser(User user, Role role) = NewUserEvent; + const factory AuthEvent.loggedOut() = LoggedOutEvent; +} + +/// {@template AuthState} +/// Complete representation of the auth page's state. +/// {@endtemplate +@Freezed() +sealed class AuthState with _$AuthState { + /// {@macro AuthState} + const factory AuthState({ + User? user, + Role? role, + }) = _AuthState; + const AuthState._(); + + /// Starter state fed to the [AuthBloc]. + factory AuthState.initial() => const AuthState(); +} diff --git a/genlatte/genlatte-ui/lib/src/core/auth/auth_bloc.freezed.dart b/genlatte/genlatte-ui/lib/src/core/auth/auth_bloc.freezed.dart new file mode 100644 index 0000000..61d15b6 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/auth/auth_bloc.freezed.dart @@ -0,0 +1,532 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auth_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$AuthEvent { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'AuthEvent()'; +} + + +} + +/// @nodoc +class $AuthEventCopyWith<$Res> { +$AuthEventCopyWith(AuthEvent _, $Res Function(AuthEvent) __); +} + + +/// Adds pattern-matching-related methods to [AuthEvent]. +extension AuthEventPatterns on AuthEvent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( NewUserEvent value)? newUser,TResult Function( LoggedOutEvent value)? loggedOut,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case NewUserEvent() when newUser != null: +return newUser(_that);case LoggedOutEvent() when loggedOut != null: +return loggedOut(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( NewUserEvent value) newUser,required TResult Function( LoggedOutEvent value) loggedOut,}){ +final _that = this; +switch (_that) { +case NewUserEvent(): +return newUser(_that);case LoggedOutEvent(): +return loggedOut(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( NewUserEvent value)? newUser,TResult? Function( LoggedOutEvent value)? loggedOut,}){ +final _that = this; +switch (_that) { +case NewUserEvent() when newUser != null: +return newUser(_that);case LoggedOutEvent() when loggedOut != null: +return loggedOut(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( User user, Role role)? newUser,TResult Function()? loggedOut,required TResult orElse(),}) {final _that = this; +switch (_that) { +case NewUserEvent() when newUser != null: +return newUser(_that.user,_that.role);case LoggedOutEvent() when loggedOut != null: +return loggedOut();case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( User user, Role role) newUser,required TResult Function() loggedOut,}) {final _that = this; +switch (_that) { +case NewUserEvent(): +return newUser(_that.user,_that.role);case LoggedOutEvent(): +return loggedOut();} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( User user, Role role)? newUser,TResult? Function()? loggedOut,}) {final _that = this; +switch (_that) { +case NewUserEvent() when newUser != null: +return newUser(_that.user,_that.role);case LoggedOutEvent() when loggedOut != null: +return loggedOut();case _: + return null; + +} +} + +} + +/// @nodoc + + +class NewUserEvent implements AuthEvent { + const NewUserEvent(this.user, this.role); + + + final User user; + final Role role; + +/// Create a copy of AuthEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NewUserEventCopyWith get copyWith => _$NewUserEventCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NewUserEvent&&(identical(other.user, user) || other.user == user)&&(identical(other.role, role) || other.role == role)); +} + + +@override +int get hashCode => Object.hash(runtimeType,user,role); + +@override +String toString() { + return 'AuthEvent.newUser(user: $user, role: $role)'; +} + + +} + +/// @nodoc +abstract mixin class $NewUserEventCopyWith<$Res> implements $AuthEventCopyWith<$Res> { + factory $NewUserEventCopyWith(NewUserEvent value, $Res Function(NewUserEvent) _then) = _$NewUserEventCopyWithImpl; +@useResult +$Res call({ + User user, Role role +}); + + + + +} +/// @nodoc +class _$NewUserEventCopyWithImpl<$Res> + implements $NewUserEventCopyWith<$Res> { + _$NewUserEventCopyWithImpl(this._self, this._then); + + final NewUserEvent _self; + final $Res Function(NewUserEvent) _then; + +/// Create a copy of AuthEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? user = null,Object? role = null,}) { + return _then(NewUserEvent( +null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable +as User,null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable +as Role, + )); +} + + +} + +/// @nodoc + + +class LoggedOutEvent implements AuthEvent { + const LoggedOutEvent(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LoggedOutEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'AuthEvent.loggedOut()'; +} + + +} + + + + +/// @nodoc +mixin _$AuthState { + + User? get user; Role? get role; +/// Create a copy of AuthState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AuthStateCopyWith get copyWith => _$AuthStateCopyWithImpl(this as AuthState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthState&&(identical(other.user, user) || other.user == user)&&(identical(other.role, role) || other.role == role)); +} + + +@override +int get hashCode => Object.hash(runtimeType,user,role); + +@override +String toString() { + return 'AuthState(user: $user, role: $role)'; +} + + +} + +/// @nodoc +abstract mixin class $AuthStateCopyWith<$Res> { + factory $AuthStateCopyWith(AuthState value, $Res Function(AuthState) _then) = _$AuthStateCopyWithImpl; +@useResult +$Res call({ + User? user, Role? role +}); + + + + +} +/// @nodoc +class _$AuthStateCopyWithImpl<$Res> + implements $AuthStateCopyWith<$Res> { + _$AuthStateCopyWithImpl(this._self, this._then); + + final AuthState _self; + final $Res Function(AuthState) _then; + +/// Create a copy of AuthState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? user = freezed,Object? role = freezed,}) { + return _then(_self.copyWith( +user: freezed == user ? _self.user : user // ignore: cast_nullable_to_non_nullable +as User?,role: freezed == role ? _self.role : role // ignore: cast_nullable_to_non_nullable +as Role?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [AuthState]. +extension AuthStatePatterns on AuthState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AuthState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AuthState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AuthState value) $default,){ +final _that = this; +switch (_that) { +case _AuthState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AuthState value)? $default,){ +final _that = this; +switch (_that) { +case _AuthState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( User? user, Role? role)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AuthState() when $default != null: +return $default(_that.user,_that.role);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( User? user, Role? role) $default,) {final _that = this; +switch (_that) { +case _AuthState(): +return $default(_that.user,_that.role);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( User? user, Role? role)? $default,) {final _that = this; +switch (_that) { +case _AuthState() when $default != null: +return $default(_that.user,_that.role);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _AuthState extends AuthState { + const _AuthState({this.user, this.role}): super._(); + + +@override final User? user; +@override final Role? role; + +/// Create a copy of AuthState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AuthStateCopyWith<_AuthState> get copyWith => __$AuthStateCopyWithImpl<_AuthState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AuthState&&(identical(other.user, user) || other.user == user)&&(identical(other.role, role) || other.role == role)); +} + + +@override +int get hashCode => Object.hash(runtimeType,user,role); + +@override +String toString() { + return 'AuthState(user: $user, role: $role)'; +} + + +} + +/// @nodoc +abstract mixin class _$AuthStateCopyWith<$Res> implements $AuthStateCopyWith<$Res> { + factory _$AuthStateCopyWith(_AuthState value, $Res Function(_AuthState) _then) = __$AuthStateCopyWithImpl; +@override @useResult +$Res call({ + User? user, Role? role +}); + + + + +} +/// @nodoc +class __$AuthStateCopyWithImpl<$Res> + implements _$AuthStateCopyWith<$Res> { + __$AuthStateCopyWithImpl(this._self, this._then); + + final _AuthState _self; + final $Res Function(_AuthState) _then; + +/// Create a copy of AuthState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? user = freezed,Object? role = freezed,}) { + return _then(_AuthState( +user: freezed == user ? _self.user : user // ignore: cast_nullable_to_non_nullable +as User?,role: freezed == role ? _self.role : role // ignore: cast_nullable_to_non_nullable +as Role?, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/core/core.dart b/genlatte/genlatte-ui/lib/src/core/core.dart new file mode 100644 index 0000000..feddce0 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/core.dart @@ -0,0 +1,8 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'app_env.dart'; +export 'auth/auth.dart'; +export 'dependency_injection.dart'; +export 'routing/routing.dart'; diff --git a/genlatte/genlatte-ui/lib/src/core/dependency_injection.dart b/genlatte/genlatte-ui/lib/src/core/dependency_injection.dart new file mode 100644 index 0000000..9e9f438 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/dependency_injection.dart @@ -0,0 +1,116 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:data_layer/data_layer.dart'; +import 'package:data_layer_hive/data_layer_hive.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:genlatte/hive/hive_adapters.dart'; +import 'package:genlatte/src/core/core.dart'; +import 'package:genlatte/src/core/utils/utils.dart'; +import 'package:genlatte/src/data/data.dart'; +import 'package:genlatte/src/data/shared_preferences_repository.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Instantiates and registers all dependencies. +Future setUpDependencyInjection({ + required AppEnv appEnv, + required FirebaseFirestore firebaseFirestore, + required FirebaseFunctions firebaseFunctions, + FirebaseAuth? firebaseAuth, + FirebaseAnalytics? firebaseAnalytics, + FirebaseRemoteConfig? firebaseRemoteConfig, +}) async { + GetIt.I.registerSingleton(appEnv); + GetIt.I.registerSingleton( + firebaseAnalytics ?? FirebaseAnalytics.instance, + ); + + final remoteConfig = firebaseRemoteConfig ?? FirebaseRemoteConfig.instance; + GetIt.I.registerSingleton(remoteConfig); + + await remoteConfig.setConfigSettings( + RemoteConfigSettings( + fetchTimeout: const Duration(minutes: 1), + minimumFetchInterval: const Duration(hours: 1), + ), + ); + await remoteConfig.fetchAndActivate(); + final contentGroup = remoteConfig.getString('contentgroup'); + + final analytics = firebaseAnalytics ?? FirebaseAnalytics.instance; + if (!kIsWeb) { + await analytics.setDefaultEventParameters({'content-group': contentGroup}); + } + + GetIt.I.registerSingleton( + firebaseAuth ?? FirebaseAuth.instance, + ); + GetIt.I.registerSingleton( + firebaseFunctions, + ); + GetIt.I.registerSingleton( + FirebaseFunctionsClient( + GetIt.I(), + appEnv: appEnv, + ), + ); + GetIt.I.registerSingleton(firebaseFirestore); + + // This must come before any repositories which use a [HiveSource]. + GetIt.I.registerSingleton(AppHiveInitializer()); + + GetIt.I.registerSingleton(AuthBloc(GetIt.I())); + + GetIt.I.registerSingleton( + AppRouter(authBloc: GetIt.I()), + ); + + GetIt.I.registerSingleton>( + LatteImageBatchRepository(), + ); + final ordersRepository = LatteOrdersRepository( + acceptImage: GetIt.I().acceptImage, + completeOrder: GetIt.I().completeOrder, + generateRevisedImages: + GetIt.I().generateRevisedImages, + rejectImageBatch: GetIt.I().rejectImageBatch, + sendToPrinters: GetIt.I().sendToPrinters, + submitOrder: GetIt.I().submitOrder, + ); + GetIt.I.registerSingleton>( + ordersRepository, + ); + GetIt.I.registerSingleton( + ordersRepository, + ); + + GetIt.I.registerSingleton>( + RecentLatteImagesRepository(), + ); + + GetIt.I.registerSingleton>( + LatteOrdersMetadataRepository(), + ); + GetIt.I.registerSingleton>( + LatteOptionsRepository(), + ); + GetIt.I.registerSingleton>( + BaristaRepository(), + ); + GetIt.I.registerSingleton>( + MachineRepository(), + ); + + final prefs = await SharedPreferences.getInstance(); + GetIt.I.registerSingleton( + SharedPreferencesRepository(prefs), + ); +} diff --git a/genlatte/genlatte-ui/lib/src/core/routing/CONTEXT.md b/genlatte/genlatte-ui/lib/src/core/routing/CONTEXT.md new file mode 100644 index 0000000..482683c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/routing/CONTEXT.md @@ -0,0 +1,24 @@ +# Routing Core + +**Purpose:** +Manages deep linking, navigation, and role-based access control inside the application using `go_router`. + +**Detailed File Overviews:** + +- `routing.dart`: + - **Description**: Barrel export file. + +- `routes.dart`: + - **Description**: Constants map defining the physical string URL routes available to the application. + +- `router.dart`: + - **Description**: Initializes the `GoRouter` instance. + - **Core Logic**: Configures a global `redirect` callback that evaluates every navigation request against the `GoRouterRedirector`, keeping users locked to screens mapped to their Firebase Auth custom claims role. Validates redirection updates dynamically using the `authBloc.stream` to catch role changes or log-outs. + +- `redirection.dart`: + - **Description**: The core Role-Based Access Control logic framework. + - **Core Logic**: Contains a suite of `Redirector` classes (like `NonBaristaAwayFromBaristaHome` and `AnonymousUsersToLogin`) that enforce strict path boundaries preventing users from guessing URLs. + +**Dependencies/Relationships:** +- Depends entirely on `AuthBloc` to determine the active user's roles. +- Consumed globally via standard `context.go()` calls across the app. diff --git a/genlatte/genlatte-ui/lib/src/core/routing/redirection.dart b/genlatte/genlatte-ui/lib/src/core/routing/redirection.dart new file mode 100644 index 0000000..0b35113 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/routing/redirection.dart @@ -0,0 +1,447 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport 'package:genlatte/src/role.dart'; +library; + +import 'package:genlatte/src/core/core.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('Redirection'); + +/// Type alias for route parameters. +typedef Params = Map; + +/// {@template GoRouterRedirector} +/// Validates that the user's current location within the app is allowed by +/// their authentication state and other details like app health. +/// {@endtemplate} +class GoRouterRedirector { + /// Singleton constructor. + factory GoRouterRedirector() => const GoRouterRedirector._( + [ + AnonymousUsersToLogin(), + AuthenticatedUsersAwayFromLogin(), + NonBaristaAwayFromBaristaHome(), + NonKioskAwayFromKioskHome(), + NonQueueObserverAwayFromQueue(), + NonRecentOrdersObserverAwayFromRecentOrders(), + NonModeratorObserverAwayFromModeration(), + NonModeratorsAwayFromPrinters(), + ], + [ + // globalMaintenanceRoute.path + // globalForceUpgradeRoute.path + ], + ); + + const GoRouterRedirector._(this._redirects, this._doNotLeave); + + final List _redirects; + + /// Forced dead-end paths that, once routed to, cannot be routed away from by + /// any other redirect rules; but instead, only by the undoing of the + /// conditions that led to redirecting here in the first place. + final List _doNotLeave; + + /// Compares the current [RouteState] against the [AuthState] and returns a + /// string to navigate to if required. Returns null if the current + /// [RouteState] and [AuthState] are compatible. + String? redirect({ + required RouteState routeState, + required AuthState authState, + }) { + _log.finest( + 'Considering redirect for "${routeState.path}" with user ' + '${authState.user?.uid} and role ${authState.role}', + ); + if (_doNotLeave.contains(routeState.path)) { + _log.finest( + 'Not navigating away from ${routeState.path} for DO NOT LEAVE', + ); + return null; + } + final current = Uri( + path: routeState.path, + queryParameters: + routeState + .uri + .queryParameters + .isNotEmpty // + ? routeState.uri.queryParameters + : null, + ); + for (final redirect in _redirects) { + if (redirect.predicate(routeState, authState)) { + final newRouteState = redirect.getNewRouteState(routeState, authState); + final uriString = newRouteState.uri.toString(); + + if (uriString == current.toString()) { + _log.warning( + '$redirect attempted to redirect to itself at $uriString. ' + 'This should have been caught earlier!', + ); + continue; + } + + _log.finer('$redirect redirecting from $current to $uriString'); + return uriString; + } else { + _log.finest( + '$redirect declined to redirect away from ${routeState.path}', + ); + } + } + _log.finer('Not redirecting away from ${routeState.path}'); + return null; + } +} + +/// {@template RouteState} +/// Simplified representation of the user's location within the app. Exists to +/// contain an individual routing solution from leaking its logic all across +/// the app's codebase. +/// {@endtemplate} +class RouteState { + /// Instantiates a new [RouteState]. + const RouteState({ + required this.uri, + this.route, + }); + + /// Converts a [GoRouterState] object into the values the rest of our app will + /// care about. + factory RouteState.fromGoRouterState(GoRouterState state) { + assert( + state.fullPath != null, + 'Unexpectedly had null [GoRouterState.fullPath]', + ); + return RouteState( + uri: state.uri, + route: state.topRoute, + ); + } + + /// Builds a GoRouterState value from a given route. + /// Useful for the initial route. + factory RouteState.fromRoute(GoRoute route, {Params? pathParameters}) { + String path = route.path; + if (pathParameters != null) { + for (final key in pathParameters.keys) { + path = path.replaceAll(':$key', pathParameters[key]!); + } + } + return RouteState( + uri: Uri(path: path), + route: route, + ); + } + + /// Fully formed URI of the route. + final Uri uri; + + /// The [GoRoute] that matches the current URI. + final GoRoute? route; + + /// Path of the route. + String get path => uri.path; + + @override + String toString() => 'RouteState(uri: $uri, route.path: ${route?.path})'; +} + +/// Individual utility within a [GoRouterRedirector] to enforce a single rule. +abstract class Redirector { + /// Const constructor. + const Redirector(); + + /// Determines whether this redirection should take place. + bool predicate(RouteState routeState, AuthState authState); + + /// Returns the desired [Uri] to send the user based on current app state. + RouteState getNewRouteState(RouteState routeState, AuthState authState); + + @override + String toString() => '$runtimeType()'; +} + +/// {@template AnonymousUsersToLogin} +/// Sends anonymous users to the login screen. +/// {@endtemplate} +class AnonymousUsersToLogin extends Redirector { + /// {@macro AnonymousUsersToLogin} + const AnonymousUsersToLogin(); + @override + bool predicate( + RouteState routeState, + AuthState authState, + ) => + routeState.path != AppRoutes.loginRoute.path && + (authState.user == null || authState.role == null); + + @override + RouteState getNewRouteState( + RouteState routeState, + AuthState authState, + ) => RouteState( + uri: Uri( + path: AppRoutes.loginRoute.path, + // Save the destination as a query parameter. + queryParameters: {'continue': routeState.uri.toString()}, + ), + route: AppRoutes.loginRoute, + ); +} + +/// {@template AuthenticatedUsersAwayFromLogin} +/// Sends authenticated users away from the login screen. +/// {@endtemplate} +class AuthenticatedUsersAwayFromLogin extends Redirector { + /// {@macro AuthenticatedUsersAwayFromLogin} + const AuthenticatedUsersAwayFromLogin(); + @override + bool predicate( + RouteState routeState, + AuthState authState, + ) => + routeState.path == AppRoutes.loginRoute.path && + authState.user != null && + authState.role != null; + + @override + RouteState getNewRouteState( + RouteState routeState, + AuthState authState, + ) { + final continueUrl = routeState.uri.queryParameters['continue']; + if (continueUrl != null) { + final uri = Uri.tryParse(continueUrl); + if (uri != null && uri.path != AppRoutes.loginRoute.path) { + _log.info('Continue parameter found, continuing to $uri'); + return RouteState(uri: uri); + } + } + + return RouteState.fromRoute( + switch (authState.role) { + null => throw Exception('Programmatic error in redirection system'), + .barista => AppRoutes.baristaHomeRoute, + .moderator => AppRoutes.moderatorHomeRoute, + .kiosk => AppRoutes.kioskHomeRoute, + .queueObserver => AppRoutes.queueRoute, + .recentOrdersObserver => AppRoutes.recentOrdersRoute, + }, + ); + } +} + +/// {@template NonBaristaAwayFromBaristaHome} +/// Sends non-barista users away from the barista home screen. This locks down +/// the barista home screen to only baristas and prevents, for example, kiosk +/// users from guessing URLs and performing Barista-only actions. +/// {@endtemplate} +class NonBaristaAwayFromBaristaHome extends Redirector { + /// {@macro NonBaristaAwayFromBaristaHome} + const NonBaristaAwayFromBaristaHome(); + @override + bool predicate( + RouteState routeState, + AuthState authState, + ) => + routeState.path == AppRoutes.baristaHomeRoute.path && + authState.role != .barista; + + @override + RouteState getNewRouteState( + RouteState routeState, + AuthState authState, + ) => RouteState.fromRoute( + switch (authState.role) { + null => AppRoutes.loginRoute, + .barista => throw Exception( + 'Programmatic error in redirection system. Barista user should not ' + 'have passed predicate for NonBaristaAwayFromBaristaHome', + ), + .moderator => AppRoutes.moderatorHomeRoute, + .kiosk => AppRoutes.kioskHomeRoute, + .queueObserver => AppRoutes.queueRoute, + .recentOrdersObserver => AppRoutes.recentOrdersRoute, + }, + ); +} + +/// {@template NonKioskAwayFromKioskHome} +/// Sends non-kiosk users away from the kiosk home screen. This locks down +/// the kiosk home screen to only kiosks and prevents, for example, the Recent +/// Orders Observer from guessing URLs and performing Kiosk-only actions. +/// {@endtemplate} +class NonKioskAwayFromKioskHome extends Redirector { + /// {@macro NonKioskAwayFromKioskHome} + const NonKioskAwayFromKioskHome(); + @override + bool predicate( + RouteState routeState, + AuthState authState, + ) => + routeState.path == AppRoutes.kioskHomeRoute.path && + authState.role != .kiosk; + + @override + RouteState getNewRouteState( + RouteState routeState, + AuthState authState, + ) => RouteState.fromRoute( + switch (authState.role) { + null => AppRoutes.loginRoute, + .barista => AppRoutes.baristaHomeRoute, + .moderator => AppRoutes.moderatorHomeRoute, + .kiosk => throw Exception( + 'Programmatic error in redirection system. Kiosk user should not ' + 'have passed predicate for NonKioskAwayFromKioskHome', + ), + .queueObserver => AppRoutes.queueRoute, + .recentOrdersObserver => AppRoutes.recentOrdersRoute, + }, + ); +} + +/// {@template NonQueueObserverAwayFromQueue} +/// Sends non-queue observer users away from the queue screen. This locks down +/// the queue screen to only queue observers and prevents... actually not much +/// in this situation since the queue observers will be elevated monitors no one +/// can easily access to hijack. But, still. Here we are. +/// {@endtemplate} +class NonQueueObserverAwayFromQueue extends Redirector { + /// {@macro NonQueueObserverAwayFromQueue} + const NonQueueObserverAwayFromQueue(); + @override + bool predicate( + RouteState routeState, + AuthState authState, + ) => + routeState.path == AppRoutes.queueRoute.path && + authState.role != .queueObserver; + + @override + RouteState getNewRouteState( + RouteState routeState, + AuthState authState, + ) => RouteState.fromRoute( + switch (authState.role) { + null => AppRoutes.loginRoute, + .barista => AppRoutes.baristaHomeRoute, + .moderator => AppRoutes.moderatorHomeRoute, + .kiosk => AppRoutes.kioskHomeRoute, + .queueObserver => throw Exception( + 'Programmatic error in redirection system. Queue observer user should ' + 'not have passed predicate for NonQueueObserverAwayFromQueue', + ), + .recentOrdersObserver => AppRoutes.recentOrdersRoute, + }, + ); +} + +/// {@template NonRecentOrdersObserverAwayFromRecentOrders} +/// Sends non-recent orders observer users away from the recent orders screen. +/// This locks down the recent orders screen to only recent orders observers +/// and prevents someone from hijacking said screen to view any of the other +/// homescreens. +/// {@endtemplate} +class NonRecentOrdersObserverAwayFromRecentOrders extends Redirector { + /// {@macro NonRecentOrdersObserverAwayFromRecentOrders} + const NonRecentOrdersObserverAwayFromRecentOrders(); + @override + bool predicate( + RouteState routeState, + AuthState authState, + ) => + routeState.path == AppRoutes.recentOrdersRoute.path && + authState.role != .recentOrdersObserver; + + @override + RouteState getNewRouteState( + RouteState routeState, + AuthState authState, + ) => RouteState.fromRoute( + switch (authState.role) { + null => AppRoutes.loginRoute, + .barista => AppRoutes.baristaHomeRoute, + .moderator => AppRoutes.moderatorHomeRoute, + .kiosk => AppRoutes.kioskHomeRoute, + .queueObserver => AppRoutes.queueRoute, + .recentOrdersObserver => throw Exception( + 'Programmatic error in redirection system. Recent orders observer ' + 'user should not have passed predicate for ' + 'NonRecentOrdersObserverAwayFromRecentOrders', + ), + }, + ); +} + +/// {@template NonModeratorObserverAwayFromModeration} +/// Sends non-moderator users away from the moderation screen. +/// This locks down the moderation screen to only moderators and prevents +/// someone from hijacking said screen to view any of the other homescreens. +/// {@endtemplate} +class NonModeratorObserverAwayFromModeration extends Redirector { + /// {@macro NonModeratorObserverAwayFromModeration} + const NonModeratorObserverAwayFromModeration(); + @override + bool predicate( + RouteState routeState, + AuthState authState, + ) => + routeState.path == AppRoutes.moderatorHomeRoute.path && + authState.role != .moderator; + + @override + RouteState getNewRouteState( + RouteState routeState, + AuthState authState, + ) => RouteState.fromRoute( + switch (authState.role) { + null => AppRoutes.loginRoute, + .barista => AppRoutes.baristaHomeRoute, + .moderator => throw Exception( + 'Programmatic error in redirection system. Moderator user should not ' + 'have passed predicate for NonModeratorObserverAwayFromModeration', + ), + .kiosk => AppRoutes.kioskHomeRoute, + .queueObserver => AppRoutes.queueRoute, + .recentOrdersObserver => AppRoutes.recentOrdersRoute, + }, + ); +} + +/// Shews non-moderators away from the printers editing view. +class NonModeratorsAwayFromPrinters extends Redirector { + /// {@macro NonModeratorsAwayFromPrinters} + const NonModeratorsAwayFromPrinters(); + @override + bool predicate( + RouteState routeState, + AuthState authState, + ) => + routeState.path == AppRoutes.moderatorPrintersRoute.path && + authState.role != .moderator; + + @override + RouteState getNewRouteState( + RouteState routeState, + AuthState authState, + ) => RouteState.fromRoute( + switch (authState.role) { + null => AppRoutes.loginRoute, + .barista => AppRoutes.baristaHomeRoute, + .moderator => throw Exception( + 'Programmatic error in redirection system. Moderator user should not ' + 'have passed predicate for NonModeratorObserverAwayFromPrinters', + ), + .kiosk => AppRoutes.kioskHomeRoute, + .queueObserver => AppRoutes.queueRoute, + .recentOrdersObserver => AppRoutes.recentOrdersRoute, + }, + ); +} diff --git a/genlatte/genlatte-ui/lib/src/core/routing/router.dart b/genlatte/genlatte-ui/lib/src/core/routing/router.dart new file mode 100644 index 0000000..b04ce4b --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/routing/router.dart @@ -0,0 +1,100 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport 'package:genlatte/src/role.dart'; +library; + +import 'dart:async'; + +import 'package:genlatte/src/core/core.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; + +final _log = Logger('AppRouter'); + +/// [GoRouter] wrapper with logging and redirection logic. +class AppRouter { + /// Instantiates a new [AppRouter]. + AppRouter({required this.authBloc}) { + redirection = GoRouterRedirector(); + + router = GoRouter( + routes: AppRoutes.routes, + initialLocation: AppRoutes.initialRoute.path, + redirect: (context, GoRouterState state) { + lastRouteState = RouteState.fromGoRouterState(state); + final redirection = _redirect(authBloc.state); + if (redirection != null) { + lastRouteState = RouteState( + uri: Uri.parse(redirection), + ); + return redirection; + } + return null; + }, + ); + + authBloc.stream.listen((authState) { + _log.finest( + 'AuthState changed: User.uid: ${authState.user?.uid} :: ' + 'Role: ${authState.role}', + ); + + // Prevent race condition: If GoRouter hasn't parsed the initial URL yet, + // do not fire programmatic redirects. The above GoRouter.redirect + // callback will handle the initial routing pass using authBloc.state. + if (lastRouteState == null) { + _log.fine( + 'GoRouter not yet initialized. Deferring to initial redirect.', + ); + return; + } + + final redirection = _redirect(authState); + if (redirection != null) { + lastRouteState = RouteState( + uri: Uri.parse(redirection), + ); + router.go(redirection); + } + }); + } + + /// {@macro AuthBloc} + final AuthBloc authBloc; + + /// [GoRouter] instance. + late final GoRouter router; + + /// Ye who keeps users from wandering off into the woods. + late final GoRouterRedirector redirection; + + /// Cache of the last known [RouteState]. + RouteState? lastRouteState; + + final _redirections = StreamController.broadcast(); + + /// Emits redirection decisions + @visibleForTesting + Stream get allRedirects => _redirections.stream; + + String? _redirect(AuthState authState) { + final redirect = redirection.redirect( + routeState: + lastRouteState ?? // + RouteState.fromRoute(AppRoutes.initialRoute), + authState: authState, + ); + _redirections.add(redirect); + return redirect; + } + + /// Redirects the user to the options editing screen. + void toOptions() => router.go('/moderator/options'); + + /// Redirects the user to the printers editing screen. If the user is not a + /// moderator, they will immediately bounce back to their own homepage. + void toMachines() => router.go('/moderator/machines'); +} diff --git a/genlatte/genlatte-ui/lib/src/core/routing/routes.dart b/genlatte/genlatte-ui/lib/src/core/routing/routes.dart new file mode 100644 index 0000000..a5c52a6 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/routing/routes.dart @@ -0,0 +1,91 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/role.dart'; +import 'package:genlatte/src/screens/barista/barista.dart'; +import 'package:genlatte/src/screens/kiosk/kiosk.dart'; +import 'package:genlatte/src/screens/login/login.dart'; +import 'package:genlatte/src/screens/moderator/moderator.dart'; +import 'package:genlatte/src/screens/queue/queue.dart'; +import 'package:genlatte/src/screens/recent_orders/recent_orders.dart'; +import 'package:go_router/go_router.dart'; + +/// {@template AppRoutes} +/// Container for all possible routes. One version of this must exist per user +/// [Role]. +/// {@endtemplate} +class AppRoutes { + /// Login route. + static final loginRoute = GoRoute( + path: '/login', + name: 'login', + builder: (context, state) => const LoginScreen(), + ); + + /// Barista home route. + static final baristaHomeRoute = GoRoute( + path: '/barista', + name: 'baristaHome', + builder: (context, state) => const BaristaHomeScreen(), + ); + + /// Moderator subscreen to manage printers. + static final moderatorPrintersRoute = GoRoute( + path: '/machines', + name: 'machines', + builder: (context, state) => const MachinesScreen(), + ); + + /// Moderator subscreen to manage options. + static final moderatorOptionsRoute = GoRoute( + path: '/options', + name: 'options', + builder: (context, state) => const OptionsScreen(), + ); + + /// Moderator home route. + static final moderatorHomeRoute = GoRoute( + path: '/moderator', + name: 'moderatorHome', + builder: (context, state) => const ModeratorHomeScreen(), + routes: [ + moderatorPrintersRoute, + moderatorOptionsRoute, + ], + ); + + /// Kiosk home route. + static final kioskHomeRoute = GoRoute( + path: '/kiosk', + name: 'kioskHome', + builder: (context, state) => const KioskHomeScreen(), + ); + + /// Queue home route. + static final queueRoute = GoRoute( + path: '/queue', + name: 'queue', + builder: (context, state) => const QueueHomeScreen(), + ); + + /// Recent orders home route. + static final recentOrdersRoute = GoRoute( + path: '/recentOrders', + name: 'recentOrders', + builder: (context, state) => const RecentOrdersHomeScreen(), + ); + + /// Starting route passed to [GoRouter]. + static final GoRoute initialRoute = loginRoute; + + /// All available routes. + static final List routes = [ + loginRoute, + baristaHomeRoute, + moderatorHomeRoute, + kioskHomeRoute, + queueRoute, + recentOrdersRoute, + ]; +} diff --git a/genlatte/genlatte-ui/lib/src/core/routing/routing.dart b/genlatte/genlatte-ui/lib/src/core/routing/routing.dart new file mode 100644 index 0000000..e14e5a3 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/routing/routing.dart @@ -0,0 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'redirection.dart'; +export 'router.dart'; +export 'routes.dart'; diff --git a/genlatte/genlatte-ui/lib/src/core/utils/CONTEXT.md b/genlatte/genlatte-ui/lib/src/core/utils/CONTEXT.md new file mode 100644 index 0000000..0b260c3 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/utils/CONTEXT.md @@ -0,0 +1,22 @@ +# Utils Core + +**Purpose:** +Contains miscellaneous shared utility functions used across the `genlatte-ui` application layer. + +**Detailed File Overviews:** + +- `utils.dart`: + - **Description**: Barrel export file. + +- `double_extensions.dart`: + - **Description**: Helper extension functions applied to the `double` primitives for standardized formatting. + +- `firebase_functions_client.dart`: + - **Description**: Helper wrapper for `FirebaseFunctions`. + - **Core Logic**: Contains standardized error catching routines used when calling HTTP-callable Cloud Functions. + +- `position.dart`: + - **Description**: A utility class abstracting `x` and `y` geometric alignments used by visual configuration steps. + +**Dependencies/Relationships:** +- General purpose scope utilized throughout the domain. diff --git a/genlatte/genlatte-ui/lib/src/core/utils/double_extensions.dart b/genlatte/genlatte-ui/lib/src/core/utils/double_extensions.dart new file mode 100644 index 0000000..cdc98f4 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/utils/double_extensions.dart @@ -0,0 +1,67 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/animation.dart'; + +/// Fancy double extensions. +extension FancyDouble on double { + /// Rounds this double to the given number of decimal places. + /// + /// Maybe there's a better way to do this, but if so, I haven't found it. + double roundTo(int decimalPlaces) => + double.parse(toStringAsFixed(decimalPlaces)); + + /// Divides [range] into a fixed number of [phases] and returns which phase + /// this double fits into. + /// + /// For usage, see `extensions_test.dart`. + /// Companion method to [progressThroughPhase]. + int phaseWithinRange( + int phases, + double range, { + double? spacePerPhase, + Curve curve = Curves.linear, + }) { + // Subtract 1 from `phases` because it is an `arr.length` situation, + // but we are returning an index. + if (this >= range) return phases - 1; + spacePerPhase ??= range / phases; + return (this / spacePerPhase).toInt(); + } + + /// Returns this double's progress through its given phase within a range. + /// + /// Usage: + /// ```dart + /// 0.05.progressThroughPhase(10, 1) => 0.5 + /// 0.1.progressThroughPhase(10, 1) => 1.0 + /// 0.15.progressThroughPhase(10, 1) => 0.5 + /// 0.16.progressThroughPhase(10, 1) => 0.6 + /// 0.26.progressThroughPhase(10, 1) => 0.6 + /// ``` + /// + /// Companion method to [phaseWithinRange]. + /// + /// If [phase] is provided, it will be used instead of calling + /// [phaseWithinRange]. Use this when your calling code also needed to know + /// the specific phase number and so already called [phaseWithinRange]. + double progressThroughPhase( + int phases, + double range, { + int? phase, + Curve curve = Curves.linear, + }) { + final spacePerPhase = range / phases; + final calculatedPhase = + phase ?? + phaseWithinRange( + phases, + range, + spacePerPhase: spacePerPhase, + ); + return curve.transform( + ((this - (spacePerPhase * calculatedPhase)) / spacePerPhase).clamp(0, 1), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/core/utils/firebase_functions_client.dart b/genlatte/genlatte-ui/lib/src/core/utils/firebase_functions_client.dart new file mode 100644 index 0000000..4c2e592 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/utils/firebase_functions_client.dart @@ -0,0 +1,132 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:genlatte/src/core/core.dart'; +import 'package:logging/logging.dart'; + +final _logger = Logger('FirebaseFunctionsClient'); + +/// A utility class for calling Firebase Functions with retry logic. +class FirebaseFunctionsClient { + /// Creates a new [FirebaseFunctionsClient]. + FirebaseFunctionsClient(this._firebaseFunctions, {required this.appEnv}); + + /// The Firebase Functions instance to use. + final FirebaseFunctions _firebaseFunctions; + + /// The active application environment. + final AppEnv appEnv; + + /// Calls 'sendToPrinters' to send an order to the printers. + Future sendToPrinters({ + required String imagePath, + required String machineName, + }) async { + await _callWithRetry( + functionName: 'sendToPrinter${appEnv.name}', + parameters: { + 'imagePath': imagePath, + 'machineName': machineName, + }, + ); + } + + /// Calls 'selectImagestaging' to accept an image. + Future acceptImage({ + required String imageBatchId, + required String imageIndex, + }) async { + await _callWithRetry( + functionName: 'selectImage${appEnv.name}', + parameters: { + 'imageBatchId': imageBatchId, + 'imageIndex': imageIndex, + }, + ); + } + + /// Calls 'completeOrder{env}' conclude the life cycle of an order. + Future completeOrder({ + required String orderId, + required String baristaId, + }) async { + await _callWithRetry( + functionName: 'completeOrder${appEnv.name}', + parameters: { + 'orderId': orderId, + 'baristaId': baristaId, + }, + ); + } + + /// Calls 'generateRevisedImagesstaging' to revise an image batch. + Future generateRevisedImages({ + required String imageBatchId, + required String imageIndex, + required Map answers, + }) async { + await _callWithRetry( + functionName: 'generateRevisedImages${appEnv.name}', + parameters: { + 'imageBatchId': imageBatchId, + 'imageIndex': imageIndex, + 'answers': answers, + }, + ); + } + + /// Calls 'rejectRevisionstaging' to reject an image batch. + Future rejectImageBatch(String imageBatchId) async { + await _callWithRetry( + functionName: 'rejectRevision${appEnv.name}', + parameters: { + 'imageBatchId': imageBatchId, + }, + ); + } + + /// Calls 'submitOrderstaging' to submit an order. + Future submitOrder(String orderId) async { + await _callWithRetry( + functionName: 'submitOrder${appEnv.name}', + parameters: { + 'orderId': orderId, + }, + ); + } + + Future _callWithRetry({ + required String functionName, + required Map parameters, + int maxRetries = 3, + }) async { + int attempt = 0; + while (true) { + try { + _logger.info( + 'Calling Firebase Function $functionName with ' + 'parameters: $parameters', + ); + final callable = _firebaseFunctions.httpsCallable(functionName); + await callable.call(parameters); + return; + } catch (e) { + attempt++; + if (attempt >= maxRetries) { + _logger.severe( + 'Failed to call Firebase Function $functionName for final time.', + e, + ); + rethrow; + } + _logger.warning( + 'Failed to call Firebase Function $functionName. Retrying...', + e, + ); + await Future.delayed(Duration(milliseconds: 500 * attempt)); + } + } + } +} diff --git a/genlatte/genlatte-ui/lib/src/core/utils/position.dart b/genlatte/genlatte-ui/lib/src/core/utils/position.dart new file mode 100644 index 0000000..db74050 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/utils/position.dart @@ -0,0 +1,169 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +/// A class that represents the position of a widget in a 2D space. +class Position { + /// Creates a new [Position] instance. + const Position({ + this.top, + this.bottom, + this.left, + this.right, + this.width, + this.height, + this.rotation, + this.transform, + this.transformAlignment, + }); + + /// The distance from the top of the parent widget to the top of this widget. + final double? top; + + /// The distance from the bottom of the parent widget to the bottom of this + /// widget. + final double? bottom; + + /// The distance from the left of the parent widget to the left of this + /// widget. + final double? left; + + /// The distance from the right of the parent widget to the right of this + /// widget. + final double? right; + + /// The width of this widget. + final double? width; + + /// The height of this widget. + final double? height; + + /// The rotation of this widget. + final double? rotation; + + /// The transform of this widget. + final Matrix4? transform; + + /// The alignment of the transform. + final Alignment? transformAlignment; + + /// Creates a new [Position] instance with the same values as this instance, + /// but with the given values replaced. + Position copyWith({ + double? top, + double? bottom, + double? left, + double? right, + double? width, + double? height, + double? rotation, + Matrix4? transform, + Alignment? transformAlignment, + }) => Position( + top: top ?? this.top, + bottom: bottom ?? this.bottom, + left: left ?? this.left, + right: right ?? this.right, + width: width ?? this.width, + height: height ?? this.height, + rotation: rotation ?? this.rotation, + transform: transform ?? this.transform, + transformAlignment: transformAlignment ?? this.transformAlignment, + ); + + /// Creates a new [Position] instance shifted in the various directions. + Position shift({ + double? top, + double? bottom, + double? left, + double? right, + }) => Position( + top: top != null ? (this.top ?? 0) + top : this.top, + bottom: bottom != null ? (this.bottom ?? 0) + bottom : this.bottom, + left: left != null ? (this.left ?? 0) + left : this.left, + right: right != null ? (this.right ?? 0) + right : this.right, + width: width, + height: height, + rotation: rotation, + transform: transform, + transformAlignment: transformAlignment, + ); + + /// Converts this [Position] to an [AnimatedPositioned] widget. + AnimatedPositioned toAnimatedWidget({ + required Widget child, + Key? key, + Curve? curve, + Duration? duration, + }) { + // ignore: no_leading_underscores_for_local_identifiers + Widget _child = child; + if (rotation != null) { + _child = Transform.rotate( + angle: rotation!, + child: _child, + ); + } + return AnimatedPositioned( + duration: duration ?? const Duration(milliseconds: 100), + curve: curve ?? Curves.easeOut, + top: top, + bottom: bottom, + left: left, + right: right, + width: width, + height: height, + key: key, + child: child, + ); + } + + /// Converts this [Position] to a [Positioned] widget. + Positioned toWidget({required Widget child, Key? key}) { + // ignore: no_leading_underscores_for_local_identifiers + Widget _child = child; + if (transform != null) { + _child = Transform( + transform: transform!, + alignment: transformAlignment ?? Alignment.topLeft, + child: _child, + ); + } + if (rotation != null) { + _child = Transform.rotate( + angle: rotation!, + child: _child, + ); + } + return Positioned( + top: top, + bottom: bottom, + left: left, + right: right, + width: width, + height: height, + key: key, + child: _child, + ); + } + + /// Converts this [Position] to a [BoxConstraints] widget. + BoxConstraints toBoxConstraints() => BoxConstraints.tightFor( + height: height, + width: width, + ); + + /// Converts this [Position] to an [Offset] widget. + Offset toOffset() => Offset( + height != null ? -height! : 0, + width != null ? width! : 0, + ); + + @override + String toString() => + 'Position(left: $left, top: $top, right: $right, bottom: $bottom, ' + 'width: $width, height: $height, rotation: $rotation, transform: ' + '$transform, transformAlignment: $transformAlignment)'; +} diff --git a/genlatte/genlatte-ui/lib/src/core/utils/utils.dart b/genlatte/genlatte-ui/lib/src/core/utils/utils.dart new file mode 100644 index 0000000..c5f1460 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/core/utils/utils.dart @@ -0,0 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'double_extensions.dart'; +export 'firebase_functions_client.dart'; +export 'position.dart'; diff --git a/genlatte/genlatte-ui/lib/src/data/CONTEXT.md b/genlatte/genlatte-ui/lib/src/data/CONTEXT.md new file mode 100644 index 0000000..6974026 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/CONTEXT.md @@ -0,0 +1,24 @@ +# Data Repositories + +**Purpose:** +Provides the "Repository Pattern" abstraction layer, exposing pure Data Models to the application's BLoC state managers while hiding external data communication implementations (like Firestore). + +**Detailed File Overviews:** + +- `data.dart`: + - **Description**: Barrel export file. + +- `latte_orders_repository.dart` / `recent_latte_images_repository.dart` / etc: + - **Description**: Domain-specific repositories. + - **Core Logic**: Wraps methods exposed by `sources/firestore_source.dart` and strictly casts payloads to/from the data structures enforced by `genlatte_data`. They often expose `Stream>` for reactive UI binding. + +- `filters.dart`: + - **Description**: Filtering logic primitives. + - **Core Logic**: Mathematical constraints passed from the Repositories down to the Sources to instruct the database to only yield specific documents (e.g. `Filter.whereEquals`). + +- `shared_preferences_repository.dart`: + - **Description**: Local storage layer. + - **Core Logic**: Wraps standard SharedPreferences for local, offline state persistence (like storing the physical hardware configurations for local Queue displays). + +**Dependencies/Relationships:** +- Serves as the intermediary between `genlatte-ui/lib/src/screens/` BLoCs and `genlatte-ui/lib/src/sources/`. diff --git a/genlatte/genlatte-ui/lib/src/data/barista_repository.dart b/genlatte/genlatte-ui/lib/src/data/barista_repository.dart new file mode 100644 index 0000000..32c6f46 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/barista_repository.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart' show FirebaseFirestore; +import 'package:data_layer/data_layer.dart'; +import 'package:data_layer_hive/data_layer_hive.dart'; +import 'package:genlatte/src/sources/firestore_source.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +/// Repository for [Barista]. +class BaristaRepository extends Repository { + /// Creates a new [BaristaRepository]. + BaristaRepository() + : super( + SourceList( + bindings: Barista.bindings, + sources: >[ + HiveSource( + hiveInit: GetIt.I().ready, + bindings: Barista.bindings, + ), + FirestoreSource( + GetIt.I(), + bindings: Barista.bindings, + ), + ], + ), + ); +} diff --git a/genlatte/genlatte-ui/lib/src/data/data.dart b/genlatte/genlatte-ui/lib/src/data/data.dart new file mode 100644 index 0000000..4851d19 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/data.dart @@ -0,0 +1,13 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'barista_repository.dart'; +export 'filters.dart'; +export 'latte_images_repository.dart'; +export 'latte_options_repository.dart'; +export 'latte_orders_metadata_repository.dart'; +export 'latte_orders_repository.dart'; +export 'machines_repository.dart'; +export 'questions_repository.dart'; +export 'recent_latte_images_repository.dart'; diff --git a/genlatte/genlatte-ui/lib/src/data/filters.dart b/genlatte/genlatte-ui/lib/src/data/filters.dart new file mode 100644 index 0000000..53321b9 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/filters.dart @@ -0,0 +1,176 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport 'package:genlatte_data/models.dart'; +library; + +import 'package:cloud_firestore/cloud_firestore.dart' hide Filter, Order; +import 'package:data_layer/data_layer.dart'; +import 'package:genlatte/src/sources/firestore_filters.dart'; +import 'package:genlatte_data/models.dart'; + +/// {@template LatteOrderMetadataBrewQueue} +/// Specifies LatteOrderMetadata documents that should show up in the barista's +/// queue to for brewing. These are moderated orders ready to be fulfilled. +/// {@endtemplate} +class LatteOrderMetadataBrewQueue extends FirestoreFilter { + /// {@macro LatteOrderMetadataBrewQueue} + const LatteOrderMetadataBrewQueue(); + + @override + Query apply(Query query) { + return query + .where( + 'status', + whereIn: [ + LatteOrderStatus.validated.name, + LatteOrderStatus.inProgress.name, + ], + ) + .where( + 'orderSubmittedTime', + isGreaterThan: Timestamp.fromDate( + DateTime.now().toUtc().subtract( + const Duration(hours: 8), + ), + ), + ) + // Note: there's no need to worry about sorting this on "moderation + // time", which a) isn't tracked, and b) should result in the same + // ordering as tracking on orderSubmittedTime anyways. + .orderBy('orderSubmittedTime', descending: true); + } + + @override + CacheKey get cacheKey => 'brew_queue'; + + @override + Json toJson() => { + 'status': { + 'whereIn': [ + LatteOrderStatus.validated.name, + LatteOrderStatus.inProgress.name, + ], + }, + 'orderSubmittedTime': { + 'isGreaterThan': DateTime.now() + .toUtc() + .subtract(const Duration(hours: 8)) + .toIso8601String(), + }, + }; + + @override + Params toParams() => toJson(); +} + +/// {@template LatteOrderMetadataBoardQueue} +/// Specifies LatteOrderMetadata documents that should show up +/// on the public queue board. +/// {@endtemplate} +class LatteOrderMetadataBoardQueue extends FirestoreFilter { + /// {@macro LatteOrderMetadataBoardQueue} + const LatteOrderMetadataBoardQueue(); + + @override + Query apply(Query query) { + return query + .where( + 'status', + whereIn: [ + LatteOrderStatus.submitted.name, + LatteOrderStatus.validated.name, + LatteOrderStatus.inProgress.name, + LatteOrderStatus.completed.name, + ], + ) + // TODO(filiph): also filter on sharding if possible + .orderBy('orderSubmittedTime', descending: true); + } + + @override + CacheKey get cacheKey => 'board_queue'; + + @override + Json toJson() => { + 'status': { + 'whereIn': [ + LatteOrderStatus.submitted.name, + LatteOrderStatus.validated.name, + LatteOrderStatus.inProgress.name, + LatteOrderStatus.completed.name, + ], + }, + }; + + @override + Params toParams() => toJson(); +} + +/// {@template LatteOrderMetadataModerationQueue} +/// Specifies LatteOrderMetadata documents that should show up in the barista's +/// queue to for moderation. Once moderated, these orders will get in the back +/// of the brew queue. +/// {@endtemplate} +class LatteOrderMetadataModerationQueue extends FirestoreFilter { + /// {@macro LatteOrderMetadataModerationQueue} + const LatteOrderMetadataModerationQueue(); + + @override + Query apply(Query query) { + return query + .where( + 'status', + whereIn: [ + LatteOrderStatus.submitted.name, + LatteOrderStatus.validated.name, + LatteOrderStatus.inProgress.name, + ], + ) + .where( + 'orderSubmittedTime', + isGreaterThan: Timestamp.fromDate( + DateTime.now().toUtc().subtract( + const Duration(hours: 8), + ), + ), + ) + .orderBy('orderSubmittedTime', descending: true); + } + + @override + CacheKey get cacheKey => 'moderation_queue'; + + @override + Json toJson() => { + 'status': { + 'whereIn': [ + LatteOrderStatus.submitted.name, + LatteOrderStatus.validated.name, + LatteOrderStatus.inProgress.name, + ], + }, + 'orderSubmittedTime': { + 'isGreaterThan': DateTime.now() + .toUtc() + .subtract(const Duration(hours: 8)) + .toIso8601String(), + }, + }; + + @override + Params toParams() => toJson(); +} + +/// Synthetic filter used to store the logged-in Barista. +class ActiveBaristaFilter extends Filter { + @override + CacheKey get cacheKey => 'active_baristas'; + + @override + Json toJson() => {'active': true}; + + @override + Params toParams() => toJson(); +} diff --git a/genlatte/genlatte-ui/lib/src/data/latte_images_repository.dart b/genlatte/genlatte-ui/lib/src/data/latte_images_repository.dart new file mode 100644 index 0000000..253506f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/latte_images_repository.dart @@ -0,0 +1,29 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:data_layer/data_layer.dart'; +import 'package:genlatte/src/sources/firestore_source.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +/// Repository for managing [LatteImageBatch]s. +class LatteImageBatchRepository extends Repository { + /// Instantiates a [LatteImageBatchRepository]. + LatteImageBatchRepository() + : super( + SourceList( + sources: [ + LocalMemorySource( + bindings: LatteImageBatch.bindings, + ), + FirestoreSource( + GetIt.I(), + bindings: LatteImageBatch.bindings, + ), + ], + bindings: LatteImageBatch.bindings, + ), + ); +} diff --git a/genlatte/genlatte-ui/lib/src/data/latte_options_repository.dart b/genlatte/genlatte-ui/lib/src/data/latte_options_repository.dart new file mode 100644 index 0000000..f5b1d20 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/latte_options_repository.dart @@ -0,0 +1,29 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart' hide Source; +import 'package:data_layer/data_layer.dart'; +import 'package:genlatte/src/sources/firestore_source.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +/// Repository for [LatteOptions]. +class LatteOptionsRepository extends Repository { + /// {@macro LatteOptionsRepository} + LatteOptionsRepository() + : super( + SourceList( + sources: >[ + LocalMemorySource( + bindings: LatteOptions.bindings, + ), + FirestoreSource( + GetIt.I(), + bindings: LatteOptions.bindings, + ), + ], + bindings: LatteOptions.bindings, + ), + ); +} diff --git a/genlatte/genlatte-ui/lib/src/data/latte_orders_metadata_repository.dart b/genlatte/genlatte-ui/lib/src/data/latte_orders_metadata_repository.dart new file mode 100644 index 0000000..9895302 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/latte_orders_metadata_repository.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart' show FirebaseFirestore; +import 'package:data_layer/data_layer.dart'; +import 'package:data_layer_hive/data_layer_hive.dart'; +import 'package:genlatte/src/sources/firestore_source.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +/// Repository for [LatteOrderMetadata]. +class LatteOrdersMetadataRepository extends Repository { + /// Creates a new [LatteOrdersMetadataRepository]. + LatteOrdersMetadataRepository() + : super( + SourceList( + bindings: LatteOrderMetadata.bindings, + sources: >[ + HiveSource( + hiveInit: GetIt.I().ready, + bindings: LatteOrderMetadata.bindings, + ), + FirestoreSource( + GetIt.I(), + bindings: LatteOrderMetadata.bindings, + ), + ], + ), + ); +} diff --git a/genlatte/genlatte-ui/lib/src/data/latte_orders_repository.dart b/genlatte/genlatte-ui/lib/src/data/latte_orders_repository.dart new file mode 100644 index 0000000..d46fef0 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/latte_orders_repository.dart @@ -0,0 +1,183 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart' show FirebaseFirestore; +import 'package:data_layer/data_layer.dart'; +import 'package:data_layer_hive/data_layer_hive.dart'; +import 'package:genlatte/src/sources/firestore_source.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +/// {@template GenerateRevisedImages} +/// Signature to answer Gemini's questions and generate revised images. +/// {@endtemplate} +typedef GenerateRevisedImages = + Future Function({ + required String imageBatchId, + required String imageIndex, // {image0, image1, image2, or image3} + required Map answers, + }); + +/// {@template RejectImageBatch} +/// Signature to reject a revised image batch and return to its parent. +/// {@endtemplate} +typedef RejectImageBatch = Future Function(String imageBatchId); + +/// {@template AcceptImage} +/// Signature to accept an image. +/// {@endtemplate} +typedef AcceptImage = + Future Function({ + required String imageBatchId, + required String imageIndex, + }); + +/// {@template SubmitOrder} +/// Signature to advance an order to the "submitted" status. +/// {@endtemplate} +typedef SubmitOrder = Future Function(String orderId); + +/// {@template SendToPrinters} +/// Signature to send an image to the printers. +/// {@endtemplate} +typedef SendToPrinters = + Future Function({ + required String imagePath, + required String machineName, + }); + +/// {@template CompleteOrder} +/// Signature to complete an order. +/// {@endtemplate} +typedef CompleteOrder = + Future Function({ + required String orderId, + required String baristaId, + }); + +/// Repository for [LatteOrder]. +class LatteOrdersRepository extends Repository { + /// Creates a new [LatteOrdersRepository]. + LatteOrdersRepository({ + required AcceptImage acceptImage, + required CompleteOrder completeOrder, + required GenerateRevisedImages generateRevisedImages, + required RejectImageBatch rejectImageBatch, + required SendToPrinters sendToPrinters, + required SubmitOrder submitOrder, + }) : _acceptImage = acceptImage, + _completeOrder = completeOrder, + _generateRevisedImages = generateRevisedImages, + _rejectImageBatch = rejectImageBatch, + _sendToPrinters = sendToPrinters, + _submitOrder = submitOrder, + super( + SourceList( + bindings: LatteOrder.bindings, + sources: >[ + HiveSource( + hiveInit: GetIt.I().ready, + bindings: LatteOrder.bindings, + ), + FirestoreSource( + GetIt.I(), + bindings: LatteOrder.bindings, + ), + ], + ), + ); + + /// {@macro AcceptImage} + Future acceptImage({ + required String imageBatchId, + required String imageIndex, + }) => _acceptImage(imageBatchId: imageBatchId, imageIndex: imageIndex); + final AcceptImage _acceptImage; + + /// {@macro CompleteOrder} + Future completeOrder({ + required String orderId, + required String baristaId, + }) => _completeOrder(orderId: orderId, baristaId: baristaId); + final CompleteOrder _completeOrder; + + /// {@macro GenerateRevisedImages} + Future generateRevisedImages({ + required String imageBatchId, + required String imageIndex, + required Map answers, + }) => _generateRevisedImages( + imageBatchId: imageBatchId, + imageIndex: imageIndex, + answers: answers, + ); + final GenerateRevisedImages _generateRevisedImages; + + /// {@macro RejectImageBatch} + Future rejectImageBatch(String imageBatchId) => + _rejectImageBatch(imageBatchId); + final RejectImageBatch _rejectImageBatch; + + /// {@macro SendToPrinter} + Future sendToPrinters({ + required String imagePath, + required String machineName, + }) => _sendToPrinters(imagePath: imagePath, machineName: machineName); + final SendToPrinters _sendToPrinters; + + /// {@macro SubmitOrder} + Future submitOrder(String orderId) => _submitOrder(orderId); + final SubmitOrder _submitOrder; + + /// Converts a list of [LatteOrderMetadata] into a list of [Latte] objects. + /// + /// This method fetches the corresponding [LatteOrder] for each + /// [LatteOrderMetadata] and bundles them into the container object, [Latte]. + /// + /// Throws a [DataException] if any [LatteOrderMetadata] objects lack a + /// corresponding [LatteOrder]. This should be caught by calling code. + Future> toLattes(List metadatas) async { + final metadataMap = metadatas.toIdsMap(); + + final (orders, missingIds) = await getByIds( + metadataMap.keys.toSet(), + // [RequestType.global] (the default value) is fine for this because + // orders no longer change once they are on the Barista's screen; so + // fetching any locally cached records is guaranteed to be fine. + ); + + if (missingIds.isNotEmpty) { + throw DataException( + 'LatteMetaData Ids $missingIds lacked corresponding LatteOrders', + ); + } + + final lattes = []; + + // It is important to loop over [orders] as returned by [getByIds] and not + // over [event.metadatas] because any records lost (as represented by + // [missingIds]) will naturally be omitted in this way. This allows us to + // guarantee that [metadataMap[order.id!]] below will not be null. + for (final order in orders) { + final metadata = metadataMap[order.id!]!; + lattes.add(Latte(order: order, metadata: metadata)); + } + + return lattes; + } +} + +/// {@template DataException} +/// Exception thrown when data is corrupted and cannot be processed. +/// {@endtemplate} +class DataException implements Exception { + /// {@macro DataException} + DataException(this.message); + + /// Description of the corrupted data. + final String message; + + @override + String toString() => 'DataException($message)'; +} diff --git a/genlatte/genlatte-ui/lib/src/data/machines_repository.dart b/genlatte/genlatte-ui/lib/src/data/machines_repository.dart new file mode 100644 index 0000000..2ae2781 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/machines_repository.dart @@ -0,0 +1,52 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart' + show FirebaseFirestore, Query; +import 'package:data_layer/data_layer.dart'; +import 'package:data_layer_hive/data_layer_hive.dart'; +import 'package:genlatte/src/sources/firestore_filters.dart'; +import 'package:genlatte/src/sources/firestore_source.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +/// Repository for [Machine]. +class MachineRepository extends Repository { + /// Creates a new [MachineRepository]. + MachineRepository() + : super( + SourceList( + bindings: Machine.bindings, + sources: >[ + HiveSource( + hiveInit: GetIt.I().ready, + bindings: Machine.bindings, + ), + FirestoreSource( + GetIt.I(), + bindings: Machine.bindings, + ), + ], + ), + ); +} + +/// Filter for active machines. +class ActiveMachinesFilter extends FirestoreFilter { + /// Instantiates a new [ActiveMachinesFilter]. + const ActiveMachinesFilter(); + + @override + Query apply(Query query) => + query.where('isActive', isEqualTo: true); + + @override + CacheKey get cacheKey => 'active_machines'; + + @override + Json toJson() => {'type': 'active_machines'}; + + @override + Params toParams() => {'isActive': 'true'}; +} diff --git a/genlatte/genlatte-ui/lib/src/data/questions_repository.dart b/genlatte/genlatte-ui/lib/src/data/questions_repository.dart new file mode 100644 index 0000000..f5d477d --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/questions_repository.dart @@ -0,0 +1,58 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// import 'package:data_layer/data_layer.dart'; +// import 'package:genlatte/src/data/data.dart'; +// import 'package:genlatte_data/models.dart'; + +// /// Repository for managing [Question]s. +// class QuestionsRepository extends Repository { +// /// Instantiates a [QuestionsRepository]. +// QuestionsRepository() +// : super( +// SourceList( +// sources: [ +// LocalMemorySource( +// bindings: Question.bindings, +// ), +// ], +// bindings: Question.bindings, +// ), +// ) { +// setItems( +// const [ +// Question.slider( +// id: 'abc', +// body: 'How hot should this be?', +// minValueLabel: 'Cold', +// maxValueLabel: 'Hot', +// ), +// Question.multipleChoice( +// id: 'xyz', +// body: 'What time of day is it?', +// options: [ +// 'Morning', +// 'Afternoon', +// 'Evening', +// 'Night', +// ], +// ), +// Question.text( +// id: '123', +// body: 'How much do you like pizza?', +// helpText: 'A lot, not so much, or somewhere in between?', +// ), +// Question.valueShiftSlider( +// id: '234', +// body: 'More, fewer, or the same amount of trees?', +// minValueLabel: 'Fewer', +// maxValueLabel: 'More', +// ), +// ], +// RequestDetails.read( +// filter: const QuestionForLatteImage(latteImageId: '1'), +// ), +// ).ignore(); +// } +// } diff --git a/genlatte/genlatte-ui/lib/src/data/recent_latte_images_repository.dart b/genlatte/genlatte-ui/lib/src/data/recent_latte_images_repository.dart new file mode 100644 index 0000000..6505919 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/recent_latte_images_repository.dart @@ -0,0 +1,59 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' show Random; + +import 'package:cloud_firestore/cloud_firestore.dart' hide Source; +import 'package:data_layer/data_layer.dart'; +import 'package:genlatte/src/sources/firestore_source.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; + +/// Repository for [LatteImage] that is used to fetch recent latte images. +class RecentLatteImagesRepository extends Repository { + /// Instantiates the [RecentLatteImagesRepository]. + RecentLatteImagesRepository() + : super( + SourceList( + bindings: RecentLatteImage.recentBindings, + sources: >[ + FirestoreSource( + GetIt.I(), + bindings: RecentLatteImage.recentBindings, + onCreateServerTimestampFields: ['createdAt'], + ), + ], + ), + ); + + final _log = Logger('RecentLatteImagesRepository'); + + /// Fake method to be deleted. + // TODO(craiglabenz): Delete this and everything around its call site once + // the recent images screen is considered 100% done. + Future addRandomRecentImage() async { + final batchRepository = GetIt.I>(); + final batches = await batchRepository.getItems(); + final randomIndex = Random().nextInt(batches.length); + final randomBatch = batches[randomIndex]; + _log.info('Random batch: ${randomBatch.id}'); + final imageIndex = Random().nextInt(4); + final randomImage = randomBatch.images.toList()[imageIndex]; + + final order = await GetIt.I>().getById( + randomBatch.orderId, + ); + + final recentImage = RecentLatteImage( + imageUrl: randomImage!.imageUrl, + prompt: randomImage.prompt, + description: randomImage.description, + createdAt: DateTime.now(), + happyPlace: order!.happyPlace!, + name: order.name!, + ); + await setItem(recentImage); + } +} diff --git a/genlatte/genlatte-ui/lib/src/data/shared_preferences_repository.dart b/genlatte/genlatte-ui/lib/src/data/shared_preferences_repository.dart new file mode 100644 index 0000000..e3301ee --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/data/shared_preferences_repository.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:logging/logging.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Repository for local settings. Backed by [SharedPreferences] +/// and not mirrored on server. +class SharedPreferencesRepository { + /// Creates a shared preferences repository. + const SharedPreferencesRepository(this._prefs); + + static final Logger _logger = Logger('$SharedPreferencesRepository'); + + final SharedPreferences _prefs; + + /// Reads a value from persistent storage, throwing an exception if it's not a + /// String. + String? getString(String key) => _prefs.getString(key); + + /// Saves a string [value] to persistent storage in the background. + Future setString(String key, String value) { + _logger.fine( + 'Setting $key to "${value.substring(0, min(200, value.length))}"', + ); + return _prefs.setString(key, value); + } +} diff --git a/genlatte/genlatte-ui/lib/src/role.dart b/genlatte/genlatte-ui/lib/src/role.dart new file mode 100644 index 0000000..1224ced --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/role.dart @@ -0,0 +1,42 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport 'package:genlatte/src/core/routing/router.dart'; +library; + +/// Roles that users can have in the app. The given role determines which +/// [AppRouter] is instantiated which in turn completely determines what app +/// experience is provided to the user. +enum Role { + /// Users who manage the queue and approve / reject user submissions. + barista, + + /// Users who can approve / reject user submissions. + moderator, + + /// Users who just need some caffeine. + kiosk, + + /// Read-only "users" who display the queue of upcoming and in-progress drinks + queueObserver, + + /// Read-only "users" who show floating bubbles of recent drinks for selfies + /// and other hi-jinks. + recentOrdersObserver; + + /// True if this role is [.barista]. + bool get isBarista => this == .barista; + + /// True if this role is [.moderator]. + bool get isModerator => this == .moderator; + + /// True if this role is [.kiosk]. + bool get isKiosk => this == .kiosk; + + /// True if this role is [.queueObserver]. + bool get isQueueObserver => this == .queueObserver; + + /// True if this role is [.recentOrdersObserver]. + bool get isRecentOrdersObserver => this == .recentOrdersObserver; +} diff --git a/genlatte/genlatte-ui/lib/src/screens/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/CONTEXT.md new file mode 100644 index 0000000..2ea6d25 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/CONTEXT.md @@ -0,0 +1,11 @@ +# Screens Features Sub-Tree + +**Purpose:** +The primary Presentation Layer source code directory encompassing every visible view inside the `genlatte-ui` package, broken out by independent user personas or global features. + +**Implementation Details:** +- **Navigation**: Each root subfolder inside (`app`, `kiosk`, `barista`, `login`, `queue`, `recent_orders`, `moderator`) serves as a distinct monolithic module backing a major user journey. +- **Architectural Uniformity**: Inside each of those subfolders, you will consistently find a structure further dividing logic into `home/` (BLoC + Views), `util/` (helpers), and `widgets/` (local view components). + +**Dependencies:** +- These view layers bind to data by calling Repositories existing in `genlatte-ui/lib/src/data/`. diff --git a/genlatte/genlatte-ui/lib/src/screens/app/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/app/CONTEXT.md new file mode 100644 index 0000000..425f1f8 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/app/CONTEXT.md @@ -0,0 +1,20 @@ +# App Screens + +**Purpose:** +Acts as the root container and top-level entry point wrapper for the entire application widget tree, injecting global themes and managing the root `MaterialApp.router`. + +**Detailed File Overviews:** + +- `app.dart`: + - **Description**: Barrel export file. + +- `app_view.dart`: + - **Description**: The core `App` widget wrapper. + - **Core Logic**: Consumes the `AppRouter` instance to construct a `ShadApp.router`, providing the top-level application branding and resolving the `theme`. + +- `theme.dart`: + - **Description**: Global styling constants. + - **Core Logic**: Provides the foundational custom typography (featuring `Unbounded` for display headers and `Open Sans` for body text) mapped against `shadcn_flutter`'s theme builder. Defines exact Hex color tokens matching branding guidelines. + +**Dependencies/Relationships:** +- Serves as the immediate child of the `runApp()` method in `lib/main.dart`. diff --git a/genlatte/genlatte-ui/lib/src/screens/app/app.dart b/genlatte/genlatte-ui/lib/src/screens/app/app.dart new file mode 100644 index 0000000..dc8254c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/app/app.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'app_view.dart'; +export 'theme.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/app/app_view.dart b/genlatte/genlatte-ui/lib/src/screens/app/app_view.dart new file mode 100644 index 0000000..09e02d3 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/app/app_view.dart @@ -0,0 +1,58 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/core/core.dart'; +import 'package:genlatte/src/screens/app/theme.dart'; +import 'package:provider/provider.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// {@template AppView} +/// Root widget for the application. +/// {@endtemplate} +class AppView extends StatelessWidget { + /// {@macro AppView} + const AppView({ + required this.authBloc, + required this.appRouter, + super.key, + }); + + /// {@macro AuthBloc} + final AuthBloc authBloc; + + /// {@macro AppRouter} + final AppRouter appRouter; + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + Provider.value(value: appRouter), + ], + child: MultiBlocProvider( + providers: [ + BlocProvider.value(value: authBloc), + ], + child: _AppView(appRouter: appRouter), + ), + ); + } +} + +class _AppView extends StatelessWidget { + const _AppView({required this.appRouter}); + + final AppRouter appRouter; + + @override + Widget build(BuildContext context) { + return ShadcnApp.router( + debugShowCheckedModeBanner: false, + title: 'GenLatte', + theme: getThemeForOrientation(context), + routerConfig: appRouter.router, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/app/theme.dart b/genlatte/genlatte-ui/lib/src/screens/app/theme.dart new file mode 100644 index 0000000..bf91403 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/app/theme.dart @@ -0,0 +1,177 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:google_fonts/google_fonts.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Colors namespace. +class AppColors { + /// Hot pink. + static const placeholderColor = Color(0xFFDD40E4); + + /// Main gold of the "GenLatte" label. + static const latteArtGold = Color(0xFFECC03B); + + /// Extremely bold yellow. + static const chevronYellow = Color(0xFFFBBC04); + + /// Google blue. + static const googleBlue = Color(0xFF5F83EE); + + /// Google red. + static const googleIntroRed = Color(0xFFD85140); + + /// Google intro blue. + static const googleIntroBlue = Color(0xFF5383EC); + + /// Google intro green. + static const googleIntroGreen = Color(0xFF58A65C); + + /// Inactive grey. + static const mutedGrey = Color(0xFF484848); + + /// Placeholder grey (black with 30% opacity). + static const placeholderGrey = Color(0x4D000000); + + /// Black. + static const black = Color(0xFF000000); + + /// Pretty close to black. + static const almostBlack = Color(0xFF121212); + + /// White. + static const white = Color(0xFFFFFFFF); + + /// Transparent. + static const transparent = Color(0x00000000); +} + +const _colorScheme = ColorScheme( + brightness: Brightness.dark, + + // Lighter color which sits behind the slightly darker circles + background: AppColors.almostBlack, + + // Base text color. + foreground: Colors.black, + card: AppColors.white, + cardForeground: AppColors.black, + popover: AppColors.white, + popoverForeground: AppColors.black, + + // Google-y Blue + primary: AppColors.googleBlue, + primaryForeground: AppColors.black, + secondary: Color(0xFFCCCCCC), + secondaryForeground: AppColors.googleBlue, + muted: AppColors.mutedGrey, + + // Text input "placeholder" color + mutedForeground: AppColors.placeholderGrey, + accent: AppColors.googleBlue, + accentForeground: AppColors.placeholderColor, + destructive: AppColors.googleIntroRed, + border: AppColors.white, + input: AppColors.white, + + // Highlight around focused elements, like text inputs + ring: AppColors.googleBlue, + chart1: AppColors.placeholderColor, + chart2: AppColors.placeholderColor, + chart3: AppColors.placeholderColor, + chart4: AppColors.placeholderColor, + chart5: AppColors.placeholderColor, +); + +/// Canonicalize an alternate color scheme which creates dark borders; useful +/// for forms with text inputs which would really benefit from having visible +/// borders. +/// +/// The issue is that buttons also use `border` for their colors; and some +/// buttons have different border colors than text inputs. +final ColorScheme _formsColorScheme = _colorScheme.copyWith( + border: () => AppColors.almostBlack, +); + +/// How them letters should look. +final Typography typography = Typography( + sans: GoogleFonts.googleSans(), + mono: GoogleFonts.jetBrainsMono(), + xSmall: const TextStyle(fontSize: 10), + small: const TextStyle(fontSize: 13.5), + base: const TextStyle(fontSize: 16), + large: const TextStyle(fontSize: 18), + xLarge: const TextStyle(fontSize: 20), + x2Large: const TextStyle(fontSize: 24), + x3Large: const TextStyle(fontSize: 30), + x4Large: const TextStyle(fontSize: 36), + x5Large: const TextStyle(fontSize: 48), + x6Large: const TextStyle(fontSize: 60), + x7Large: const TextStyle(fontSize: 72), + x8Large: const TextStyle(fontSize: 96), + x9Large: const TextStyle(fontSize: 144), + thin: const TextStyle(fontWeight: FontWeight.w100), + light: const TextStyle(fontWeight: FontWeight.w300), + extraLight: const TextStyle(fontWeight: FontWeight.w200), + normal: const TextStyle(fontWeight: FontWeight.w400), + medium: const TextStyle(fontWeight: FontWeight.w500), + semiBold: const TextStyle(fontWeight: FontWeight.w600), + bold: const TextStyle(fontWeight: FontWeight.w700), + extraBold: const TextStyle(fontWeight: FontWeight.w800), + black: const TextStyle(fontWeight: FontWeight.w900), + italic: const TextStyle(fontStyle: FontStyle.italic), + + h1: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.w800, + color: AppColors.black, + ), + h2: const TextStyle( + fontSize: 36, + fontWeight: FontWeight.w600, + color: AppColors.black, + ), + h3: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: AppColors.black, + ), + // Used for app bars of pages, which are intentionally smaller than other + // headers used for prominent cards + h4: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.w400, + color: AppColors.latteArtGold, + ), + p: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400), + blockQuote: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + fontStyle: FontStyle.italic, + ), + inlineCode: GoogleFonts.jetBrainsMono().copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + lead: const TextStyle(fontSize: 20), + textLarge: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + textSmall: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + textMuted: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400), +); + +/// Returns the appropriate theme for the current orientation. +ThemeData getThemeForOrientation(BuildContext context) { + return ThemeData( + colorScheme: _colorScheme, + typography: typography, + radius: 0.6, + ); +} + +/// Special theme for forms with text inputs which are otherwise white-on-white. +final ThemeData formsTheme = ThemeData( + colorScheme: _formsColorScheme, + typography: typography, + radius: 0.6, +); diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/barista/CONTEXT.md new file mode 100644 index 0000000..04df99c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/CONTEXT.md @@ -0,0 +1,11 @@ +# Kiosk / Barista / Moderator / Queue / Recent Orders Roots + +**Purpose:** +This directory acts as the architectural boundary grouping for the application's distinct personas. + +**Implementation Details:** +- **Barrel Architecture**: This level strictly exports child module directories like `home/` and `widgets/` via an `index` / `barrel` file. +- **Child Contexts**: Please navigate into `home/` or `widgets/` subdirectories to view specific BLoC architectures, View layouts, and complex logic files tailored towards each app persona. + +**Dependencies:** +- Consumed by `core/routing/routes.dart` to map physical URLs to specific screen entry points. diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/barista.dart b/genlatte/genlatte-ui/lib/src/screens/barista/barista.dart new file mode 100644 index 0000000..828b3c1 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/barista.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'home/barista_home.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/home/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/barista/home/CONTEXT.md new file mode 100644 index 0000000..4609e51 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/home/CONTEXT.md @@ -0,0 +1,28 @@ +# Barista Home + +**Purpose:** +Manages the application state, business logic, and UI view for the primary Barista interface. This area empowers baristas to sign in, select connected physical machines (printers), and pull/claim orders from the unified queue. + +**Detailed File Overviews:** + +- `barista_home.dart`: + - **Description**: A barrel export file. + - **Usage/Exports**: Exposes `barista_home_bloc.dart` and `barista_home_view.dart`. + +- `barista_home_bloc.dart`: + - **Description**: The core state management unit (BLoC) governing the barista's workflow. + - **Core Logic**: Handles multiple asynchronous streams: fetching the latest `LatteOrderMetadata` (brew queue), active `Barista` list, and available active `Machine`s. Contains extensive logic to sign in a barista, cache locally with Hive, bind them to a selected printer, and claim/complete specific orders (`ClaimOrder`, `CompleteOrder` events). + - **Exposed Methods**: Consumes events like `NewBrewQueueOrders`, `BaristaSignIn`, `ClaimOrder`, and `SelectedMachine`, subsequently yielding new `BaristaHomeState` frames. + +- `barista_home_bloc.freezed.dart`: + - **Description**: The automatically generated freezed boilerplate for `BaristaHomeEvent` and `BaristaHomeState` immutability and union types. + +- `barista_home_view.dart`: + - **Description**: Defines the `BaristaHomeScreen`, integrating `BaristaHomeBloc` state directly into the UI. + - **Core Logic**: Dynamically switches the layout depending on the `currentBarista` state—showing a `BaristaPersonaCard` login screen if null, or transitioning to the `InternalOrderQueues.barista` queue view if authenticated. Also spawns modal dialogs to compel machine selection if the barista hasn't connected to a printer. + - **Dependencies**: Depends heavily on `barista/widgets` (e.g. `BaristaPersonaCard`) and `widgets/InternalOrderQueues`. + +**Dependencies/Relationships:** +- Closely tied to the `genlatte_data` package repositories (`LatteOrdersRepository`, `Repository`). +- Depends on sibling module `barista/widgets` for specific UI modules. +- Coordinates with Firebase Auth for sign out capability. diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home.dart b/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home.dart new file mode 100644 index 0000000..cc13f5e --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'barista_home_bloc.dart'; +export 'barista_home_view.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home_bloc.dart b/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home_bloc.dart new file mode 100644 index 0000000..55c2d2b --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home_bloc.dart @@ -0,0 +1,426 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte/src/data/data.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; +import 'package:uuid/uuid.dart'; + +part 'barista_home_bloc.freezed.dart'; + +final _logger = Logger('BaristaHomeBloc'); + +typedef _Emit = Emitter; + +/// {@template BaristaHomeBloc} +/// Bloc for the Barista Home screen. +/// {@endtemplate} +class BaristaHomeBloc extends Bloc { + /// {@macro BaristaHomeBloc} + BaristaHomeBloc() : super(BaristaHomeState.initial()) { + on( + (event, _Emit emit) => switch (event) { + NewBrewQueueOrders() => _onNewBrewQueueOrders(event, emit), + NewBaristas() => _onNewBaristas(event, emit), + BaristaSignIn() => _onBaristaSignIn(event, emit), + BaristaSignOut() => _onBaristaSignOut(event, emit), + ClaimOrder() => _onClaimOrder(event, emit), + CompleteOrder() => _onCompleteOrder(event, emit), + ReprintOrder() => _onReprintOrder(event, emit), + NewMachineList() => _onNewMachineList(event, emit), + SelectedMachineDisappeared() => _onSelectedMachineDisappeared( + event, + emit, + ), + SelectedMachine() => _onSelectedMachine(event, emit), + }, + ); + + _metadataRepository = GetIt.I>(); + _ordersRepository = GetIt.I(); + + _brewStreamSub = _metadataRepository + .watchList( + details: RequestDetails.read( + filter: const LatteOrderMetadataBrewQueue(), + ), + ) + .listen((metadata) => add(NewBrewQueueOrders(metadata))); + + _baristasRepository = GetIt.I>(); + + _baristaStreamSub = _baristasRepository.watchList().listen( + (baristas) => add(NewBaristas(baristas)), + ); + + _baristasRepository + .getItems( + details: RequestDetails( + filter: ActiveBaristaFilter(), + requestType: .local, + ), + ) + .then( + (baristas) { + if (baristas.length == 1) { + add(BaristaSignIn(baristas.first)); + } + }, + ) + .ignore(); + + _machinesRepository = GetIt.I>(); + + _machinesStreamSub = _machinesRepository + .watchList( + details: RequestDetails.read( + filter: const ActiveMachinesFilter(), + ), + ) + .listen( + (machines) => add(NewMachineList(machines)), + ); + } + + late final Repository _machinesRepository; + late final StreamSubscription> _machinesStreamSub; + + late final Repository _baristasRepository; + late final LatteOrdersRepository _ordersRepository; + late final Repository _metadataRepository; + + late final StreamSubscription> _brewStreamSub; + late final StreamSubscription> _baristaStreamSub; + + Future _onNewBrewQueueOrders( + NewBrewQueueOrders event, + _Emit emit, + ) async { + _logger.fine( + 'New brew queue: ${event.metadatas.map((m) => m.id!)}', + ); + + List lattes; + try { + lattes = await _ordersRepository.toLattes(event.metadatas); + } on DataException catch (e) { + _logger.severe(e.message); + return; + } + + emit( + state.copyWith( + brewQueue: lattes + // Older orders appear first + ..sort( + (a, b) => a.metadata.orderSubmittedTime!.compareTo( + b.metadata.orderSubmittedTime!, + ), + ), + ), + ); + } + + void _onNewBaristas(NewBaristas event, _Emit emit) { + _logger.fine( + 'New baristas queue: ${event.baristas} :: ' + 'currentBarista: [${state.currentBarista}]', + ); + emit(state.copyWith(baristas: event.baristas.toIdsMap())); + } + + Future _onBaristaSignIn(BaristaSignIn event, _Emit emit) async { + final allBaristas = await _baristasRepository.getItems( + details: RequestDetails.read(requestType: .allLocal), + ); + + Barista? signedInBarista; + for (final barista in allBaristas) { + if (barista.username == event.barista.username && + barista.persona == event.barista.persona) { + _logger.fine('Logging in user as existing barista: $barista'); + signedInBarista = barista; + break; + } + } + if (signedInBarista == null) { + final savedBarista = await _baristasRepository.setItem(event.barista); + if (savedBarista == null) { + _logger.severe( + 'Failed to save barista ${event.barista}. Received null from ' + '[setItem].', + ); + return; + } + signedInBarista = savedBarista; + } + emit(state.copyWith(currentBarista: signedInBarista)); + + /// Cache this locally, which will write it to Hive but not bother + /// sending the payload to Firestore. + await _baristasRepository.setItems( + [signedInBarista], + RequestDetails( + filter: ActiveBaristaFilter(), + requestType: .local, + ), + ); + } + + Future _onBaristaSignOut(BaristaSignOut event, _Emit emit) async { + // Remove the active Barista from the local cache. + await _baristasRepository.delete( + state.currentBarista!.id!, + RequestDetails( + filter: ActiveBaristaFilter(), + requestType: .local, + ), + ); + emit(state.copyWith(currentBarista: null)); + } + + Future _onClaimOrder(ClaimOrder event, _Emit emit) async { + if (state.selectedMachine == null) { + return emit( + state.copyWith( + error: MachinesError.machineDisconnected(), + selectedMachine: null, + ), + ); + } + + final latte = state.brewQueue.firstWhereOrNull( + (l) => l.order.id == event.orderId, + ); + + if (latte == null) { + _logger.warning( + 'Could not find latte for order id ${event.orderId}. ' + 'Maybe another barista claimed it at the same time?', + ); + return; + } + + final metadata = latte.metadata.copyWith( + baristaId: state.currentBarista?.id, + status: .inProgress, + ); + + final imageUrlToPrint = latte.metadata.isImageApproved == false + ? 'https://firebasestorage.googleapis.com/v0/b/gcdemos-26-int-dd-latteart.firebasestorage.app/o/latteImages%2FfallbackImage%2Ficon_flutter.png?alt=media&token=06a3ac7e-7929-4507-8105-9eddb4445892' + : latte.metadata.imageUrl!; + + // No need to capture the returned metadata, since it will get picked up + // by our open `watch` streams. + await _metadataRepository.setItem(metadata); + + await _ordersRepository.sendToPrinters( + imagePath: imageUrlToPrint, + machineName: state.selectedMachine!.name, + ); + } + + Future _onCompleteOrder(CompleteOrder event, _Emit emit) async { + final latte = state.brewQueue.firstWhereOrNull( + (l) => l.order.id == event.orderId, + ); + + if (latte == null) { + _logger.warning( + 'Could not find latte for order id ${event.orderId}. ' + 'Maybe another barista completed it at the same time?', + ); + return; + } + + /// Optimistic update to immediately remove the order from the UI. + emit( + state.copyWith( + brewQueue: state.brewQueue + .where((l) => l.order.id != event.orderId) + .toList(), + ), + ); + + await _ordersRepository.completeOrder( + orderId: latte.order.id!, + baristaId: state.currentBarista!.id!, + ); + } + + Future _onReprintOrder(ReprintOrder event, _Emit emit) async { + if (state.selectedMachine == null) { + return emit( + state.copyWith( + error: MachinesError.machineDisconnected(), + selectedMachine: null, + ), + ); + } + + final latte = state.brewQueue.firstWhereOrNull( + (l) => l.order.id == event.orderId, + ); + + if (latte == null) { + _logger.warning( + 'Could not find latte for order id ${event.orderId}. ' + 'Maybe another barista completed it at the same time?', + ); + return; + } + + final imageUrlToPrint = latte.metadata.isImageApproved == false + ? 'https://firebasestorage.googleapis.com/v0/b/gcdemos-26-int-dd-latteart.firebasestorage.app/o/latteImages%2FfallbackImage%2Ficon_flutter.png?alt=media&token=06a3ac7e-7929-4507-8105-9eddb4445892' + : latte.metadata.imageUrl!; + + await _ordersRepository.sendToPrinters( + imagePath: imageUrlToPrint, + machineName: state.selectedMachine!.name, + ); + } + + void _onNewMachineList(NewMachineList event, _Emit emit) { + final machinesIdMap = event.machines.toIdsMap(); + final selectedMachine = state.selectedMachine; + if (selectedMachine != null && + !machinesIdMap.containsKey(selectedMachine.id)) { + add(const SelectedMachineDisappeared()); + } + emit(state.copyWith(machines: event.machines)); + } + + void _onSelectedMachineDisappeared( + SelectedMachineDisappeared event, + _Emit emit, + ) { + emit( + state.copyWith( + error: MachinesError.machineDisconnected(), + selectedMachine: null, + ), + ); + } + + void _onSelectedMachine(SelectedMachine event, _Emit emit) { + emit(state.copyWith(selectedMachine: event.machine, error: null)); + } + + @override + Future close() { + _brewStreamSub.cancel().ignore(); + _baristaStreamSub.cancel().ignore(); + _machinesStreamSub.cancel().ignore(); + return super.close(); + } +} + +/// Actions that can be taken on the BaristaHome page. +@Freezed() +sealed class BaristaHomeEvent with _$BaristaHomeEvent { + /// A new list of ready-to-brew orders has arrived from the server. + const factory BaristaHomeEvent.newBrewQueueOrders( + List metadatas, + ) = NewBrewQueueOrders; + + /// A new list of ready-to-brew orders has arrived from the server. + const factory BaristaHomeEvent.newMachinesList( + List machines, + ) = NewMachineList; + + /// The previously selected machine is no longer available. + const factory BaristaHomeEvent.selectedMachineDisappeared() = + SelectedMachineDisappeared; + + /// The barista has selected a new machine. + const factory BaristaHomeEvent.selectedMachine(Machine machine) = + SelectedMachine; + + /// A new list of Baristas has arrived from the server. + const factory BaristaHomeEvent.newBaristas(List baristas) = + NewBaristas; + + /// A new barista has signed in and selected their persona. + const factory BaristaHomeEvent.baristaSignIn(Barista barista) = BaristaSignIn; + + /// A barista has ended their shift. + const factory BaristaHomeEvent.baristaSignOut() = BaristaSignOut; + + /// The barista has claimed an order. + const factory BaristaHomeEvent.claimOrder(String orderId) = ClaimOrder; + + /// The barista has completed an order. + const factory BaristaHomeEvent.completeOrder(String orderId) = CompleteOrder; + + /// The barista has requested to reprint an order. + const factory BaristaHomeEvent.reprintOrder(String orderId) = ReprintOrder; +} + +/// {@template BaristaHomeState} +/// Complete representation of the BaristaHome page's state. +/// {@endtemplate +@Freezed() +sealed class BaristaHomeState with _$BaristaHomeState { + /// {@macro BaristaHomeState} + const factory BaristaHomeState({ + required List brewQueue, + Barista? currentBarista, + @Default({}) Map baristas, + @Default([]) List machines, + Machine? selectedMachine, + MachinesError? error, + }) = _BaristaHomeState; + const BaristaHomeState._(); + + /// Starter state fed to the [BaristaHomeBloc]. + factory BaristaHomeState.initial({List? brewQueue}) => + BaristaHomeState(brewQueue: brewQueue ?? []); +} + +extension _MyList on List { + /// Returns the first matching element, or null if there are no matches.` + E? firstWhereOrNull(bool Function(E element) test, {E? Function()? orElse}) { + for (final E element in this) { + if (test(element)) return element; + } + if (orElse != null) return orElse(); + return null; + } +} + +/// Error to show a Barista regarding their printer. The Uuid is just for +/// deduplication so the UI doesn't spam the same toast over and over. +class MachinesError { + /// Instantiates a [MachinesError]. + factory MachinesError.machineDisconnected() => MachinesError._( + 'The selected machine is no longer available. ' + 'Please choose another one.', + .machineDisconnected, + const Uuid().v4(), + ); + + MachinesError._(this.message, this.code, this.uuid); + + /// The message to show the barista. + final String message; + + /// The code associated with the error. + final ErrorCode code; + + /// Id for deduplication purposes in the widget tree. + final String uuid; +} + +/// Codes to identify the type of error. The UI uses this to know concretely +/// what action a Barista needs to take to rectify the error. +enum ErrorCode { + /// The Barista's previously-connected machine is no longer available. + machineDisconnected, +} diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home_bloc.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home_bloc.freezed.dart new file mode 100644 index 0000000..a01bb7e --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home_bloc.freezed.dart @@ -0,0 +1,1186 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'barista_home_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$BaristaHomeEvent { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BaristaHomeEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'BaristaHomeEvent()'; +} + + +} + +/// @nodoc +class $BaristaHomeEventCopyWith<$Res> { +$BaristaHomeEventCopyWith(BaristaHomeEvent _, $Res Function(BaristaHomeEvent) __); +} + + +/// Adds pattern-matching-related methods to [BaristaHomeEvent]. +extension BaristaHomeEventPatterns on BaristaHomeEvent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( NewBrewQueueOrders value)? newBrewQueueOrders,TResult Function( NewMachineList value)? newMachinesList,TResult Function( SelectedMachineDisappeared value)? selectedMachineDisappeared,TResult Function( SelectedMachine value)? selectedMachine,TResult Function( NewBaristas value)? newBaristas,TResult Function( BaristaSignIn value)? baristaSignIn,TResult Function( BaristaSignOut value)? baristaSignOut,TResult Function( ClaimOrder value)? claimOrder,TResult Function( CompleteOrder value)? completeOrder,TResult Function( ReprintOrder value)? reprintOrder,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case NewBrewQueueOrders() when newBrewQueueOrders != null: +return newBrewQueueOrders(_that);case NewMachineList() when newMachinesList != null: +return newMachinesList(_that);case SelectedMachineDisappeared() when selectedMachineDisappeared != null: +return selectedMachineDisappeared(_that);case SelectedMachine() when selectedMachine != null: +return selectedMachine(_that);case NewBaristas() when newBaristas != null: +return newBaristas(_that);case BaristaSignIn() when baristaSignIn != null: +return baristaSignIn(_that);case BaristaSignOut() when baristaSignOut != null: +return baristaSignOut(_that);case ClaimOrder() when claimOrder != null: +return claimOrder(_that);case CompleteOrder() when completeOrder != null: +return completeOrder(_that);case ReprintOrder() when reprintOrder != null: +return reprintOrder(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( NewBrewQueueOrders value) newBrewQueueOrders,required TResult Function( NewMachineList value) newMachinesList,required TResult Function( SelectedMachineDisappeared value) selectedMachineDisappeared,required TResult Function( SelectedMachine value) selectedMachine,required TResult Function( NewBaristas value) newBaristas,required TResult Function( BaristaSignIn value) baristaSignIn,required TResult Function( BaristaSignOut value) baristaSignOut,required TResult Function( ClaimOrder value) claimOrder,required TResult Function( CompleteOrder value) completeOrder,required TResult Function( ReprintOrder value) reprintOrder,}){ +final _that = this; +switch (_that) { +case NewBrewQueueOrders(): +return newBrewQueueOrders(_that);case NewMachineList(): +return newMachinesList(_that);case SelectedMachineDisappeared(): +return selectedMachineDisappeared(_that);case SelectedMachine(): +return selectedMachine(_that);case NewBaristas(): +return newBaristas(_that);case BaristaSignIn(): +return baristaSignIn(_that);case BaristaSignOut(): +return baristaSignOut(_that);case ClaimOrder(): +return claimOrder(_that);case CompleteOrder(): +return completeOrder(_that);case ReprintOrder(): +return reprintOrder(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( NewBrewQueueOrders value)? newBrewQueueOrders,TResult? Function( NewMachineList value)? newMachinesList,TResult? Function( SelectedMachineDisappeared value)? selectedMachineDisappeared,TResult? Function( SelectedMachine value)? selectedMachine,TResult? Function( NewBaristas value)? newBaristas,TResult? Function( BaristaSignIn value)? baristaSignIn,TResult? Function( BaristaSignOut value)? baristaSignOut,TResult? Function( ClaimOrder value)? claimOrder,TResult? Function( CompleteOrder value)? completeOrder,TResult? Function( ReprintOrder value)? reprintOrder,}){ +final _that = this; +switch (_that) { +case NewBrewQueueOrders() when newBrewQueueOrders != null: +return newBrewQueueOrders(_that);case NewMachineList() when newMachinesList != null: +return newMachinesList(_that);case SelectedMachineDisappeared() when selectedMachineDisappeared != null: +return selectedMachineDisappeared(_that);case SelectedMachine() when selectedMachine != null: +return selectedMachine(_that);case NewBaristas() when newBaristas != null: +return newBaristas(_that);case BaristaSignIn() when baristaSignIn != null: +return baristaSignIn(_that);case BaristaSignOut() when baristaSignOut != null: +return baristaSignOut(_that);case ClaimOrder() when claimOrder != null: +return claimOrder(_that);case CompleteOrder() when completeOrder != null: +return completeOrder(_that);case ReprintOrder() when reprintOrder != null: +return reprintOrder(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( List metadatas)? newBrewQueueOrders,TResult Function( List machines)? newMachinesList,TResult Function()? selectedMachineDisappeared,TResult Function( Machine machine)? selectedMachine,TResult Function( List baristas)? newBaristas,TResult Function( Barista barista)? baristaSignIn,TResult Function()? baristaSignOut,TResult Function( String orderId)? claimOrder,TResult Function( String orderId)? completeOrder,TResult Function( String orderId)? reprintOrder,required TResult orElse(),}) {final _that = this; +switch (_that) { +case NewBrewQueueOrders() when newBrewQueueOrders != null: +return newBrewQueueOrders(_that.metadatas);case NewMachineList() when newMachinesList != null: +return newMachinesList(_that.machines);case SelectedMachineDisappeared() when selectedMachineDisappeared != null: +return selectedMachineDisappeared();case SelectedMachine() when selectedMachine != null: +return selectedMachine(_that.machine);case NewBaristas() when newBaristas != null: +return newBaristas(_that.baristas);case BaristaSignIn() when baristaSignIn != null: +return baristaSignIn(_that.barista);case BaristaSignOut() when baristaSignOut != null: +return baristaSignOut();case ClaimOrder() when claimOrder != null: +return claimOrder(_that.orderId);case CompleteOrder() when completeOrder != null: +return completeOrder(_that.orderId);case ReprintOrder() when reprintOrder != null: +return reprintOrder(_that.orderId);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( List metadatas) newBrewQueueOrders,required TResult Function( List machines) newMachinesList,required TResult Function() selectedMachineDisappeared,required TResult Function( Machine machine) selectedMachine,required TResult Function( List baristas) newBaristas,required TResult Function( Barista barista) baristaSignIn,required TResult Function() baristaSignOut,required TResult Function( String orderId) claimOrder,required TResult Function( String orderId) completeOrder,required TResult Function( String orderId) reprintOrder,}) {final _that = this; +switch (_that) { +case NewBrewQueueOrders(): +return newBrewQueueOrders(_that.metadatas);case NewMachineList(): +return newMachinesList(_that.machines);case SelectedMachineDisappeared(): +return selectedMachineDisappeared();case SelectedMachine(): +return selectedMachine(_that.machine);case NewBaristas(): +return newBaristas(_that.baristas);case BaristaSignIn(): +return baristaSignIn(_that.barista);case BaristaSignOut(): +return baristaSignOut();case ClaimOrder(): +return claimOrder(_that.orderId);case CompleteOrder(): +return completeOrder(_that.orderId);case ReprintOrder(): +return reprintOrder(_that.orderId);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( List metadatas)? newBrewQueueOrders,TResult? Function( List machines)? newMachinesList,TResult? Function()? selectedMachineDisappeared,TResult? Function( Machine machine)? selectedMachine,TResult? Function( List baristas)? newBaristas,TResult? Function( Barista barista)? baristaSignIn,TResult? Function()? baristaSignOut,TResult? Function( String orderId)? claimOrder,TResult? Function( String orderId)? completeOrder,TResult? Function( String orderId)? reprintOrder,}) {final _that = this; +switch (_that) { +case NewBrewQueueOrders() when newBrewQueueOrders != null: +return newBrewQueueOrders(_that.metadatas);case NewMachineList() when newMachinesList != null: +return newMachinesList(_that.machines);case SelectedMachineDisappeared() when selectedMachineDisappeared != null: +return selectedMachineDisappeared();case SelectedMachine() when selectedMachine != null: +return selectedMachine(_that.machine);case NewBaristas() when newBaristas != null: +return newBaristas(_that.baristas);case BaristaSignIn() when baristaSignIn != null: +return baristaSignIn(_that.barista);case BaristaSignOut() when baristaSignOut != null: +return baristaSignOut();case ClaimOrder() when claimOrder != null: +return claimOrder(_that.orderId);case CompleteOrder() when completeOrder != null: +return completeOrder(_that.orderId);case ReprintOrder() when reprintOrder != null: +return reprintOrder(_that.orderId);case _: + return null; + +} +} + +} + +/// @nodoc + + +class NewBrewQueueOrders implements BaristaHomeEvent { + const NewBrewQueueOrders(final List metadatas): _metadatas = metadatas; + + + final List _metadatas; + List get metadatas { + if (_metadatas is EqualUnmodifiableListView) return _metadatas; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_metadatas); +} + + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NewBrewQueueOrdersCopyWith get copyWith => _$NewBrewQueueOrdersCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NewBrewQueueOrders&&const DeepCollectionEquality().equals(other._metadatas, _metadatas)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_metadatas)); + +@override +String toString() { + return 'BaristaHomeEvent.newBrewQueueOrders(metadatas: $metadatas)'; +} + + +} + +/// @nodoc +abstract mixin class $NewBrewQueueOrdersCopyWith<$Res> implements $BaristaHomeEventCopyWith<$Res> { + factory $NewBrewQueueOrdersCopyWith(NewBrewQueueOrders value, $Res Function(NewBrewQueueOrders) _then) = _$NewBrewQueueOrdersCopyWithImpl; +@useResult +$Res call({ + List metadatas +}); + + + + +} +/// @nodoc +class _$NewBrewQueueOrdersCopyWithImpl<$Res> + implements $NewBrewQueueOrdersCopyWith<$Res> { + _$NewBrewQueueOrdersCopyWithImpl(this._self, this._then); + + final NewBrewQueueOrders _self; + final $Res Function(NewBrewQueueOrders) _then; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? metadatas = null,}) { + return _then(NewBrewQueueOrders( +null == metadatas ? _self._metadatas : metadatas // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class NewMachineList implements BaristaHomeEvent { + const NewMachineList(final List machines): _machines = machines; + + + final List _machines; + List get machines { + if (_machines is EqualUnmodifiableListView) return _machines; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_machines); +} + + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NewMachineListCopyWith get copyWith => _$NewMachineListCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NewMachineList&&const DeepCollectionEquality().equals(other._machines, _machines)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_machines)); + +@override +String toString() { + return 'BaristaHomeEvent.newMachinesList(machines: $machines)'; +} + + +} + +/// @nodoc +abstract mixin class $NewMachineListCopyWith<$Res> implements $BaristaHomeEventCopyWith<$Res> { + factory $NewMachineListCopyWith(NewMachineList value, $Res Function(NewMachineList) _then) = _$NewMachineListCopyWithImpl; +@useResult +$Res call({ + List machines +}); + + + + +} +/// @nodoc +class _$NewMachineListCopyWithImpl<$Res> + implements $NewMachineListCopyWith<$Res> { + _$NewMachineListCopyWithImpl(this._self, this._then); + + final NewMachineList _self; + final $Res Function(NewMachineList) _then; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? machines = null,}) { + return _then(NewMachineList( +null == machines ? _self._machines : machines // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class SelectedMachineDisappeared implements BaristaHomeEvent { + const SelectedMachineDisappeared(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SelectedMachineDisappeared); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'BaristaHomeEvent.selectedMachineDisappeared()'; +} + + +} + + + + +/// @nodoc + + +class SelectedMachine implements BaristaHomeEvent { + const SelectedMachine(this.machine); + + + final Machine machine; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SelectedMachineCopyWith get copyWith => _$SelectedMachineCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SelectedMachine&&(identical(other.machine, machine) || other.machine == machine)); +} + + +@override +int get hashCode => Object.hash(runtimeType,machine); + +@override +String toString() { + return 'BaristaHomeEvent.selectedMachine(machine: $machine)'; +} + + +} + +/// @nodoc +abstract mixin class $SelectedMachineCopyWith<$Res> implements $BaristaHomeEventCopyWith<$Res> { + factory $SelectedMachineCopyWith(SelectedMachine value, $Res Function(SelectedMachine) _then) = _$SelectedMachineCopyWithImpl; +@useResult +$Res call({ + Machine machine +}); + + +$MachineCopyWith<$Res> get machine; + +} +/// @nodoc +class _$SelectedMachineCopyWithImpl<$Res> + implements $SelectedMachineCopyWith<$Res> { + _$SelectedMachineCopyWithImpl(this._self, this._then); + + final SelectedMachine _self; + final $Res Function(SelectedMachine) _then; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? machine = null,}) { + return _then(SelectedMachine( +null == machine ? _self.machine : machine // ignore: cast_nullable_to_non_nullable +as Machine, + )); +} + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MachineCopyWith<$Res> get machine { + + return $MachineCopyWith<$Res>(_self.machine, (value) { + return _then(_self.copyWith(machine: value)); + }); +} +} + +/// @nodoc + + +class NewBaristas implements BaristaHomeEvent { + const NewBaristas(final List baristas): _baristas = baristas; + + + final List _baristas; + List get baristas { + if (_baristas is EqualUnmodifiableListView) return _baristas; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_baristas); +} + + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NewBaristasCopyWith get copyWith => _$NewBaristasCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NewBaristas&&const DeepCollectionEquality().equals(other._baristas, _baristas)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_baristas)); + +@override +String toString() { + return 'BaristaHomeEvent.newBaristas(baristas: $baristas)'; +} + + +} + +/// @nodoc +abstract mixin class $NewBaristasCopyWith<$Res> implements $BaristaHomeEventCopyWith<$Res> { + factory $NewBaristasCopyWith(NewBaristas value, $Res Function(NewBaristas) _then) = _$NewBaristasCopyWithImpl; +@useResult +$Res call({ + List baristas +}); + + + + +} +/// @nodoc +class _$NewBaristasCopyWithImpl<$Res> + implements $NewBaristasCopyWith<$Res> { + _$NewBaristasCopyWithImpl(this._self, this._then); + + final NewBaristas _self; + final $Res Function(NewBaristas) _then; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? baristas = null,}) { + return _then(NewBaristas( +null == baristas ? _self._baristas : baristas // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class BaristaSignIn implements BaristaHomeEvent { + const BaristaSignIn(this.barista); + + + final Barista barista; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BaristaSignInCopyWith get copyWith => _$BaristaSignInCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BaristaSignIn&&(identical(other.barista, barista) || other.barista == barista)); +} + + +@override +int get hashCode => Object.hash(runtimeType,barista); + +@override +String toString() { + return 'BaristaHomeEvent.baristaSignIn(barista: $barista)'; +} + + +} + +/// @nodoc +abstract mixin class $BaristaSignInCopyWith<$Res> implements $BaristaHomeEventCopyWith<$Res> { + factory $BaristaSignInCopyWith(BaristaSignIn value, $Res Function(BaristaSignIn) _then) = _$BaristaSignInCopyWithImpl; +@useResult +$Res call({ + Barista barista +}); + + +$BaristaCopyWith<$Res> get barista; + +} +/// @nodoc +class _$BaristaSignInCopyWithImpl<$Res> + implements $BaristaSignInCopyWith<$Res> { + _$BaristaSignInCopyWithImpl(this._self, this._then); + + final BaristaSignIn _self; + final $Res Function(BaristaSignIn) _then; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? barista = null,}) { + return _then(BaristaSignIn( +null == barista ? _self.barista : barista // ignore: cast_nullable_to_non_nullable +as Barista, + )); +} + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$BaristaCopyWith<$Res> get barista { + + return $BaristaCopyWith<$Res>(_self.barista, (value) { + return _then(_self.copyWith(barista: value)); + }); +} +} + +/// @nodoc + + +class BaristaSignOut implements BaristaHomeEvent { + const BaristaSignOut(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BaristaSignOut); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'BaristaHomeEvent.baristaSignOut()'; +} + + +} + + + + +/// @nodoc + + +class ClaimOrder implements BaristaHomeEvent { + const ClaimOrder(this.orderId); + + + final String orderId; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ClaimOrderCopyWith get copyWith => _$ClaimOrderCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ClaimOrder&&(identical(other.orderId, orderId) || other.orderId == orderId)); +} + + +@override +int get hashCode => Object.hash(runtimeType,orderId); + +@override +String toString() { + return 'BaristaHomeEvent.claimOrder(orderId: $orderId)'; +} + + +} + +/// @nodoc +abstract mixin class $ClaimOrderCopyWith<$Res> implements $BaristaHomeEventCopyWith<$Res> { + factory $ClaimOrderCopyWith(ClaimOrder value, $Res Function(ClaimOrder) _then) = _$ClaimOrderCopyWithImpl; +@useResult +$Res call({ + String orderId +}); + + + + +} +/// @nodoc +class _$ClaimOrderCopyWithImpl<$Res> + implements $ClaimOrderCopyWith<$Res> { + _$ClaimOrderCopyWithImpl(this._self, this._then); + + final ClaimOrder _self; + final $Res Function(ClaimOrder) _then; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? orderId = null,}) { + return _then(ClaimOrder( +null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class CompleteOrder implements BaristaHomeEvent { + const CompleteOrder(this.orderId); + + + final String orderId; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CompleteOrderCopyWith get copyWith => _$CompleteOrderCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CompleteOrder&&(identical(other.orderId, orderId) || other.orderId == orderId)); +} + + +@override +int get hashCode => Object.hash(runtimeType,orderId); + +@override +String toString() { + return 'BaristaHomeEvent.completeOrder(orderId: $orderId)'; +} + + +} + +/// @nodoc +abstract mixin class $CompleteOrderCopyWith<$Res> implements $BaristaHomeEventCopyWith<$Res> { + factory $CompleteOrderCopyWith(CompleteOrder value, $Res Function(CompleteOrder) _then) = _$CompleteOrderCopyWithImpl; +@useResult +$Res call({ + String orderId +}); + + + + +} +/// @nodoc +class _$CompleteOrderCopyWithImpl<$Res> + implements $CompleteOrderCopyWith<$Res> { + _$CompleteOrderCopyWithImpl(this._self, this._then); + + final CompleteOrder _self; + final $Res Function(CompleteOrder) _then; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? orderId = null,}) { + return _then(CompleteOrder( +null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class ReprintOrder implements BaristaHomeEvent { + const ReprintOrder(this.orderId); + + + final String orderId; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ReprintOrderCopyWith get copyWith => _$ReprintOrderCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ReprintOrder&&(identical(other.orderId, orderId) || other.orderId == orderId)); +} + + +@override +int get hashCode => Object.hash(runtimeType,orderId); + +@override +String toString() { + return 'BaristaHomeEvent.reprintOrder(orderId: $orderId)'; +} + + +} + +/// @nodoc +abstract mixin class $ReprintOrderCopyWith<$Res> implements $BaristaHomeEventCopyWith<$Res> { + factory $ReprintOrderCopyWith(ReprintOrder value, $Res Function(ReprintOrder) _then) = _$ReprintOrderCopyWithImpl; +@useResult +$Res call({ + String orderId +}); + + + + +} +/// @nodoc +class _$ReprintOrderCopyWithImpl<$Res> + implements $ReprintOrderCopyWith<$Res> { + _$ReprintOrderCopyWithImpl(this._self, this._then); + + final ReprintOrder _self; + final $Res Function(ReprintOrder) _then; + +/// Create a copy of BaristaHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? orderId = null,}) { + return _then(ReprintOrder( +null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc +mixin _$BaristaHomeState { + + List get brewQueue; Barista? get currentBarista; Map get baristas; List get machines; Machine? get selectedMachine; MachinesError? get error; +/// Create a copy of BaristaHomeState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BaristaHomeStateCopyWith get copyWith => _$BaristaHomeStateCopyWithImpl(this as BaristaHomeState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BaristaHomeState&&const DeepCollectionEquality().equals(other.brewQueue, brewQueue)&&(identical(other.currentBarista, currentBarista) || other.currentBarista == currentBarista)&&const DeepCollectionEquality().equals(other.baristas, baristas)&&const DeepCollectionEquality().equals(other.machines, machines)&&(identical(other.selectedMachine, selectedMachine) || other.selectedMachine == selectedMachine)&&(identical(other.error, error) || other.error == error)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(brewQueue),currentBarista,const DeepCollectionEquality().hash(baristas),const DeepCollectionEquality().hash(machines),selectedMachine,error); + +@override +String toString() { + return 'BaristaHomeState(brewQueue: $brewQueue, currentBarista: $currentBarista, baristas: $baristas, machines: $machines, selectedMachine: $selectedMachine, error: $error)'; +} + + +} + +/// @nodoc +abstract mixin class $BaristaHomeStateCopyWith<$Res> { + factory $BaristaHomeStateCopyWith(BaristaHomeState value, $Res Function(BaristaHomeState) _then) = _$BaristaHomeStateCopyWithImpl; +@useResult +$Res call({ + List brewQueue, Barista? currentBarista, Map baristas, List machines, Machine? selectedMachine, MachinesError? error +}); + + +$BaristaCopyWith<$Res>? get currentBarista;$MachineCopyWith<$Res>? get selectedMachine; + +} +/// @nodoc +class _$BaristaHomeStateCopyWithImpl<$Res> + implements $BaristaHomeStateCopyWith<$Res> { + _$BaristaHomeStateCopyWithImpl(this._self, this._then); + + final BaristaHomeState _self; + final $Res Function(BaristaHomeState) _then; + +/// Create a copy of BaristaHomeState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? brewQueue = null,Object? currentBarista = freezed,Object? baristas = null,Object? machines = null,Object? selectedMachine = freezed,Object? error = freezed,}) { + return _then(_self.copyWith( +brewQueue: null == brewQueue ? _self.brewQueue : brewQueue // ignore: cast_nullable_to_non_nullable +as List,currentBarista: freezed == currentBarista ? _self.currentBarista : currentBarista // ignore: cast_nullable_to_non_nullable +as Barista?,baristas: null == baristas ? _self.baristas : baristas // ignore: cast_nullable_to_non_nullable +as Map,machines: null == machines ? _self.machines : machines // ignore: cast_nullable_to_non_nullable +as List,selectedMachine: freezed == selectedMachine ? _self.selectedMachine : selectedMachine // ignore: cast_nullable_to_non_nullable +as Machine?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as MachinesError?, + )); +} +/// Create a copy of BaristaHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$BaristaCopyWith<$Res>? get currentBarista { + if (_self.currentBarista == null) { + return null; + } + + return $BaristaCopyWith<$Res>(_self.currentBarista!, (value) { + return _then(_self.copyWith(currentBarista: value)); + }); +}/// Create a copy of BaristaHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MachineCopyWith<$Res>? get selectedMachine { + if (_self.selectedMachine == null) { + return null; + } + + return $MachineCopyWith<$Res>(_self.selectedMachine!, (value) { + return _then(_self.copyWith(selectedMachine: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [BaristaHomeState]. +extension BaristaHomeStatePatterns on BaristaHomeState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _BaristaHomeState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _BaristaHomeState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _BaristaHomeState value) $default,){ +final _that = this; +switch (_that) { +case _BaristaHomeState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _BaristaHomeState value)? $default,){ +final _that = this; +switch (_that) { +case _BaristaHomeState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List brewQueue, Barista? currentBarista, Map baristas, List machines, Machine? selectedMachine, MachinesError? error)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _BaristaHomeState() when $default != null: +return $default(_that.brewQueue,_that.currentBarista,_that.baristas,_that.machines,_that.selectedMachine,_that.error);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List brewQueue, Barista? currentBarista, Map baristas, List machines, Machine? selectedMachine, MachinesError? error) $default,) {final _that = this; +switch (_that) { +case _BaristaHomeState(): +return $default(_that.brewQueue,_that.currentBarista,_that.baristas,_that.machines,_that.selectedMachine,_that.error);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List brewQueue, Barista? currentBarista, Map baristas, List machines, Machine? selectedMachine, MachinesError? error)? $default,) {final _that = this; +switch (_that) { +case _BaristaHomeState() when $default != null: +return $default(_that.brewQueue,_that.currentBarista,_that.baristas,_that.machines,_that.selectedMachine,_that.error);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _BaristaHomeState extends BaristaHomeState { + const _BaristaHomeState({required final List brewQueue, this.currentBarista, final Map baristas = const {}, final List machines = const [], this.selectedMachine, this.error}): _brewQueue = brewQueue,_baristas = baristas,_machines = machines,super._(); + + + final List _brewQueue; +@override List get brewQueue { + if (_brewQueue is EqualUnmodifiableListView) return _brewQueue; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_brewQueue); +} + +@override final Barista? currentBarista; + final Map _baristas; +@override@JsonKey() Map get baristas { + if (_baristas is EqualUnmodifiableMapView) return _baristas; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_baristas); +} + + final List _machines; +@override@JsonKey() List get machines { + if (_machines is EqualUnmodifiableListView) return _machines; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_machines); +} + +@override final Machine? selectedMachine; +@override final MachinesError? error; + +/// Create a copy of BaristaHomeState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$BaristaHomeStateCopyWith<_BaristaHomeState> get copyWith => __$BaristaHomeStateCopyWithImpl<_BaristaHomeState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _BaristaHomeState&&const DeepCollectionEquality().equals(other._brewQueue, _brewQueue)&&(identical(other.currentBarista, currentBarista) || other.currentBarista == currentBarista)&&const DeepCollectionEquality().equals(other._baristas, _baristas)&&const DeepCollectionEquality().equals(other._machines, _machines)&&(identical(other.selectedMachine, selectedMachine) || other.selectedMachine == selectedMachine)&&(identical(other.error, error) || other.error == error)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_brewQueue),currentBarista,const DeepCollectionEquality().hash(_baristas),const DeepCollectionEquality().hash(_machines),selectedMachine,error); + +@override +String toString() { + return 'BaristaHomeState(brewQueue: $brewQueue, currentBarista: $currentBarista, baristas: $baristas, machines: $machines, selectedMachine: $selectedMachine, error: $error)'; +} + + +} + +/// @nodoc +abstract mixin class _$BaristaHomeStateCopyWith<$Res> implements $BaristaHomeStateCopyWith<$Res> { + factory _$BaristaHomeStateCopyWith(_BaristaHomeState value, $Res Function(_BaristaHomeState) _then) = __$BaristaHomeStateCopyWithImpl; +@override @useResult +$Res call({ + List brewQueue, Barista? currentBarista, Map baristas, List machines, Machine? selectedMachine, MachinesError? error +}); + + +@override $BaristaCopyWith<$Res>? get currentBarista;@override $MachineCopyWith<$Res>? get selectedMachine; + +} +/// @nodoc +class __$BaristaHomeStateCopyWithImpl<$Res> + implements _$BaristaHomeStateCopyWith<$Res> { + __$BaristaHomeStateCopyWithImpl(this._self, this._then); + + final _BaristaHomeState _self; + final $Res Function(_BaristaHomeState) _then; + +/// Create a copy of BaristaHomeState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? brewQueue = null,Object? currentBarista = freezed,Object? baristas = null,Object? machines = null,Object? selectedMachine = freezed,Object? error = freezed,}) { + return _then(_BaristaHomeState( +brewQueue: null == brewQueue ? _self._brewQueue : brewQueue // ignore: cast_nullable_to_non_nullable +as List,currentBarista: freezed == currentBarista ? _self.currentBarista : currentBarista // ignore: cast_nullable_to_non_nullable +as Barista?,baristas: null == baristas ? _self._baristas : baristas // ignore: cast_nullable_to_non_nullable +as Map,machines: null == machines ? _self._machines : machines // ignore: cast_nullable_to_non_nullable +as List,selectedMachine: freezed == selectedMachine ? _self.selectedMachine : selectedMachine // ignore: cast_nullable_to_non_nullable +as Machine?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as MachinesError?, + )); +} + +/// Create a copy of BaristaHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$BaristaCopyWith<$Res>? get currentBarista { + if (_self.currentBarista == null) { + return null; + } + + return $BaristaCopyWith<$Res>(_self.currentBarista!, (value) { + return _then(_self.copyWith(currentBarista: value)); + }); +}/// Create a copy of BaristaHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MachineCopyWith<$Res>? get selectedMachine { + if (_self.selectedMachine == null) { + return null; + } + + return $MachineCopyWith<$Res>(_self.selectedMachine!, (value) { + return _then(_self.copyWith(selectedMachine: value)); + }); +} +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home_view.dart b/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home_view.dart new file mode 100644 index 0000000..a4ca330 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/home/barista_home_view.dart @@ -0,0 +1,251 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/barista/home/barista_home.dart'; +import 'package:genlatte/src/screens/barista/widgets/widgets.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart' show GetIt; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// {@template BaristaHomeScreen} +/// Initial BaristaHome screen. +/// {@endtemplate} +class BaristaHomeScreen extends StatefulWidget { + /// {@macro BaristaHomeScreen} + const BaristaHomeScreen({super.key}); + + @override + State createState() => _BaristaHomeScreenState(); +} + +class _BaristaHomeScreenState extends State { + final BaristaHomeBloc bloc = BaristaHomeBloc(); + + final shownErrorUuids = {}; + + bool _hasPerformedInitialMachineCheck = false; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: bloc, + builder: (context, state) { + final size = MediaQuery.sizeOf(context); + return BlocListener( + bloc: bloc, + listener: (context, state) { + if (state.error != null && + !shownErrorUuids.contains(state.error!.uuid)) { + shownErrorUuids.add(state.error!.uuid); + _showErrorToast(state.error!, state.machines).ignore(); + } + + if (!_hasPerformedInitialMachineCheck && + state.machines.isNotEmpty && + state.selectedMachine == null) { + _hasPerformedInitialMachineCheck = true; + _showMachineSelectionToast(state.machines).ignore(); + } + }, + child: Scaffold( + headers: [ + if (state.currentBarista != null) + AppBar( + backgroundColor: AppColors.almostBlack, + leading: [ + Avatar( + initials: state.currentBarista!.username.substring(0, 1), + provider: AssetImage( + state.currentBarista!.persona.assetName, + ), + ), + const SizedBox(width: 8), + if (state.selectedMachine != null) + GenLatteOutlinedButton.light( + label: + '${state.selectedMachine!.name} ' + // ignore: lines_longer_than_80_chars + '${state.selectedMachine!.isBlackAndWhite ? '🔲' : '🎨'}', + onPressed: () => _showMachineSelectionToast( + state.machines, + selectedMachine: state.selectedMachine, + ), + ), + if (state.selectedMachine == null) + GenLatteOutlinedButton.red( + label: 'Select a Machine', + onPressed: () => _showMachineSelectionToast( + state.machines, + ), + ), + ], + title: Center( + child: const Text( + 'Barista Queue', + style: TextStyle(color: AppColors.white), + ).h3, + ), + trailing: [ + GenLatteOutlinedButton.light( + label: 'End your watch', + onPressed: () => bloc.add(const BaristaSignOut()), + ), + ], + ), + ], + child: state.currentBarista == null + ? Padding( + padding: EdgeInsets.symmetric( + horizontal: size.width * 0.15, + vertical: size.height * 0.1, + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + flex: 10, + child: BaristaPersonaCard( + onSubmit: (username, persona) => bloc.add( + BaristaSignIn( + Barista(username: username, persona: persona), + ), + ), + ), + ), + Expanded( + child: GhostButton( + onPressed: () => + GetIt.I().signOut(), + child: const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: Text( + 'Logout', + style: TextStyle( + fontSize: 20, + color: AppColors.white, + fontWeight: FontWeight.w600, + letterSpacing: 0.7, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ) + : InternalOrderQueues.barista( + activeBarista: state.currentBarista!, + canClaimOrders: state.selectedMachine != null, + baristas: state.baristas, + orders: state.brewQueue, + onClaimPressed: (orderId) async { + bloc.add(ClaimOrder(orderId)); + await GetIt.I.get().logEvent( + name: 'barista_claim_order', + ); + }, + onCompletePressed: (orderId) async { + bloc.add(CompleteOrder(orderId)); + await GetIt.I.get().logEvent( + name: 'barista_complete_order', + ); + }, + onReprintPressed: (orderId) async { + bloc.add(ReprintOrder(orderId)); + await GetIt.I.get().logEvent( + name: 'barista_reprint_order', + ); + }, + ), + ), + ); + }, + ); + } + + Future _showMachineSelectionToast( + List machines, { + Machine? selectedMachine, + }) { + return showDialog( + context: context, + builder: (context) { + final size = MediaQuery.sizeOf(context); + return SizedBox( + width: min(640, size.width * 0.8), + child: AlertDialog( + title: const Text('Select a Machine').bold, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + MachineSelection( + machines: machines, + initialSelection: selectedMachine, + onSubmitted: (machine) { + bloc.add(SelectedMachine(machine)); + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _showErrorToast( + MachinesError error, + List machines, + ) async { + switch (error.code) { + case ErrorCode.machineDisconnected: + await showDialog( + context: context, + builder: (context) { + final size = MediaQuery.sizeOf(context); + return SizedBox( + width: min(640, size.width * 0.8), + child: AlertDialog( + title: const Text('Machine Disconnected').bold, + content: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, + children: [ + Text(error.message), + const SizedBox(height: 16), + MachineSelection( + machines: machines, + onSubmitted: (machine) { + bloc.add(SelectedMachine(machine)); + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + }, + ); + } + } + + @override + Future dispose() async { + await bloc.close(); + super.dispose(); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/widgets/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/CONTEXT.md new file mode 100644 index 0000000..ef3fcb4 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/CONTEXT.md @@ -0,0 +1,36 @@ +# Barista Widgets + +**Purpose:** +A repository of reusable widgets tailored specifically for the barista-facing application interfaces. These components elegantly display orders, present the barista persona, and visualize the status of the local queues. + +**Detailed File Overviews:** + +- `barista_persona_card.dart`: + - **Description**: UI component for setting up a barista persona (authentication). + - **Core Logic**: Manages a local text controller for username input and a selectable grid of AI-generated avatars. Applies grayscale filters to unselected avatars using `ColorFilter.matrix` and triggers an `onSubmit` callback upon completion. + - **Usage/Exports**: Used heavily by the `BaristaHomeScreen` to instantiate a Barista session. + +- `humanized_countdown.dart`: + - **Description**: Widget rendering a time differential (e.g. "5m ago"). + +- `image_stack.dart`: + - **Description**: Displays the generated latte image overlaid with the customer's name. + - **Core Logic**: Reads `LatteOrderMetadata` to determine if an image was approved or rejected by moderation. Injects fallback URLs if needed, and applies colored banners visually denoting rejection statuses. + +- `internal_order_queues.dart`: + - **Description**: Renders horizontal, segregated scroll lists displaying cards for all orders ready for moderation or brewing. + - **Core Logic**: Handles role-based visibility. For `barista` roles, it only prints the brew queue. For `moderator` roles, it also prints the moderation queue. Loops over a list of `Latte` objects, outputting either barista or moderator variants of `LatteOrderCard`. + +- `latte_order_card.dart`: + - **Description**: The core visual representation of a single latte order. + - **Core Logic**: Intensely stateful due to a built-in flipping animation (using `Matrix4` rotation/perspective), providing a dual-sided card. One side (`_LatteOrderCardInner.moderate`) exposes AI moderation controls (Approve/Reject), and the reverse side (`_LatteOrderCardInner.brew`) displays physical brew buttons (Claim/Complete), a badge for the assigned barista, and details regarding Milk/Sweetener selections. + +- `order_details_table.dart`: + - **Description**: A tabular layout for printing the `OrderDetailItem` list (Milk, Sweetener, Drink Type). + +- `widgets.dart`: + - **Description**: Barrel export file. + +**Dependencies/Relationships:** +- Strongly coupled to `genlatte_data`'s data models (`Latte`, `LatteOrderMetadata`, `LatteImageBatch`). +- Intended for consumption strictly by `genlatte-ui/lib/src/screens/barista` and `moderator` screens. diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/widgets/barista_persona_card.dart b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/barista_persona_card.dart new file mode 100644 index 0000000..85e5fae --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/barista_persona_card.dart @@ -0,0 +1,252 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// {@template BaristaPersonaCard} +/// A card for setting up a barista persona. +/// {@endtemplate} +class BaristaPersonaCard extends StatefulWidget { + /// {@macro BaristaPersonaCard} + const BaristaPersonaCard({ + required this.onSubmit, + super.key, + }); + + /// Callback for when the user submits their persona. + final void Function(String username, BaristaPersona persona) onSubmit; + + @override + State createState() => _BaristaPersonaCardState(); +} + +class _BaristaPersonaCardState extends State { + final TextEditingController _usernameController = TextEditingController(); + int? _selectedAvatarIndex; + + @override + void initState() { + super.initState(); + _usernameController.addListener(_onUsernameChanged); + } + + void _onUsernameChanged() { + setState(() {}); + } + + @override + void dispose() { + _usernameController.dispose(); + super.dispose(); + } + + // Generated by Gemini. Who knows. + static const greyScaleMatrix = [ + ...[0.2126, 0.7152, 0.0722, 0], + ...[0, 0.2126, 0.7152, 0.0722], + ...[0, 0, 0.2126, 0.7152], + ...[0.0722, 0, 0, 0], + ...[0, 0, 1, 0], + ]; + + static const _idealSize = Size(520, 520); + + @override + Widget build(BuildContext context) { + return ResponsiveSizedBox( + aspectRatioClamp: (null, _idealSize.aspectRatio, null), + child: LayoutProvider.builder( + builder: (context, layoutInfo) { + final scale = layoutInfo.constrainedScale(_idealSize); + return Container( + width: layoutInfo.width, + padding: EdgeInsets.symmetric( + horizontal: 24 * scale, + vertical: 32 * scale, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20 * scale), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 25 * scale, + offset: Offset(0, 10 * scale), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header + Text( + 'Set up your profile', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24 * scale, + fontWeight: FontWeight.bold, + color: const Color(0xFF111827), + ), + ), + SizedBox(height: 8 * scale), + Text( + 'Choose a username and your avatar.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14 * scale, + color: const Color(0xFF6B7280), + ), + ), + const SizedBox(height: 28), + + // Username Input + Text( + 'Username', + style: TextStyle( + fontSize: 13 * scale, + fontWeight: FontWeight.w600, + color: const Color(0xFF374151), + ), + ), + SizedBox(height: 8 * scale), + TextField( + controller: _usernameController, + decoration: BoxDecoration( + border: Border.all( + color: AppColors.mutedGrey, + ), // defaults to BorderStyle.none + borderRadius: BorderRadius.all( + Radius.circular(8 * scale), + ), + color: Theme.of(context).colorScheme.input, + ), + // onChanged: widget.onSelected, + placeholder: Text( + 'e.g. Your name', + style: TextStyle(fontSize: 16 * scale), + ), + padding: EdgeInsets.symmetric( + horizontal: 16 * scale, + vertical: 12 * scale, + ), + style: TextStyle( + fontSize: 16 * scale, + ), + ), + SizedBox(height: 24 * scale), + + // Avatar Grid + Text( + 'Select Avatar', + style: TextStyle( + fontSize: 13 * scale, + fontWeight: FontWeight.w600, + color: const Color(0xFF374151), + ), + ), + SizedBox(height: 8 * scale), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: BaristaPersona.values.length, + itemBuilder: (context, index) { + final isSelected = _selectedAvatarIndex == index; + return GestureDetector( + onTap: () { + setState(() { + _selectedAvatarIndex = index; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + transform: isSelected + // ignore: deprecated_member_use + ? (Matrix4.identity()..scale(1.05)) + : Matrix4.identity(), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? const Color(0xFF3B82F6) + : Colors.transparent, + width: 3 * scale, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: const Color( + 0xFF3B82F6, + ).withValues(alpha: 0.3), + blurRadius: 10 * scale, + offset: Offset(0, 4 * scale), + ), + ] + : [], + ), + child: ClipOval( + child: ColorFiltered( + // Applies a grayscale effect to unselected avatars + colorFilter: isSelected + ? const ColorFilter.mode( + Colors.transparent, + BlendMode.multiply, + ) + : const ColorFilter.matrix(greyScaleMatrix), + child: Image.asset( + BaristaPersona.values[index].assetName, + fit: BoxFit.cover, + ), + ), + ), + ), + ); + }, + ), + SizedBox(height: 32 * scale), + + // Submit Button + PrimaryButton( + onPressed: + _usernameController.text.isNotEmpty && + _selectedAvatarIndex != null + ? () { + widget.onSubmit( + _usernameController.text, + BaristaPersona.values[_selectedAvatarIndex!], + ); + } + : null, + child: Center( + child: Padding( + padding: EdgeInsets.all(8 * scale), + child: Text( + 'Get started', + style: TextStyle( + fontSize: 20 * scale, + color: AppColors.white, + fontWeight: FontWeight.w600, + letterSpacing: 0.7, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/widgets/humanized_countdown.dart b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/humanized_countdown.dart new file mode 100644 index 0000000..b133e9b --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/humanized_countdown.dart @@ -0,0 +1,84 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:flutter/material.dart'; + +/// A widget that displays a countdown timer in a human-readable format. +class HumanizedCountdown extends StatefulWidget { + /// Instantiates a [HumanizedCountdown]. + const HumanizedCountdown({ + required this.timestamp, + this.label, + this.textStyle, + super.key, + }); + + /// Optional label. + final String? label; + + /// Start time. + final DateTime timestamp; + + /// Optional styling. + final TextStyle? textStyle; + + @override + State createState() => _HumanizedCountdownState(); +} + +class _HumanizedCountdownState extends State { + late Timer _timer; + + @override + void initState() { + super.initState(); + // Update the UI every second + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (mounted) setState(() {}); + }); + } + + @override + void dispose() { + _timer.cancel(); + super.dispose(); + } + + String _formatDuration(DateTime localizedTime) { + final now = DateTime.now(); + final duration = now.difference(localizedTime); + + // Guard against future dates + if (duration.isNegative || duration.inSeconds < 1) { + return 'Just now'; + } + + final int sec = duration.inSeconds % 60; + final int min = duration.inMinutes % 60; + final int hour = duration.inHours % 24; + final int days = duration.inDays; + + if (days > 0) { + return '${days}d ${hour}h ago'; + } else if (hour > 0) { + return '${hour}h ${min}m ago'; + } else if (min > 0) { + return '${min}m ${sec}s ago'; + } else { + return '${sec}s ago'; + } + } + + @override + Widget build(BuildContext context) { + return Text( + '${widget.label != null ? "${widget.label} " : ''}' + '${_formatDuration(widget.timestamp)}', + style: + widget.textStyle ?? + const TextStyle(fontSize: 16, color: Colors.blueGrey), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/widgets/image_stack.dart b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/image_stack.dart new file mode 100644 index 0000000..e7d2551 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/image_stack.dart @@ -0,0 +1,101 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: use_if_null_to_convert_nulls_to_bools + +import 'dart:math' show min; + +import 'package:genlatte_data/models.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' hide IconButton; + +/// Widget to display the generated latte image with the name overlayed. +class ImageStack extends StatelessWidget { + /// Instantiates a [ImageStack] widget. + const ImageStack({required this.scaleFactor, required this.latte, super.key}); + + /// The scale factor to apply to the widget. + final double scaleFactor; + + /// The latte to display. + final Latte latte; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SizedBox.square( + dimension: min( + constraints.maxWidth, + constraints.maxHeight, + ), + child: Stack( + children: [ + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular( + 8 * scaleFactor, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Image.network( + latte.metadata.isImageApproved == false + ? 'https://firebasestorage.googleapis.com/v0/b/gcdemos-26-int-dd-latteart.firebasestorage.app/o/latteImages%2FfallbackImage%2Ficon_flutter.png?alt=media&token=06a3ac7e-7929-4507-8105-9eddb4445892' + : latte.metadata.imageUrl!, + fit: BoxFit.cover, + ), + ], + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: EdgeInsets.all(8 * scaleFactor), + color: latte.metadata.isNameApproved != false + ? Colors.black.withValues(alpha: 0.5) + : Colors.red.withValues(alpha: 0.5), + child: Text( + latte.order.name!, + style: TextStyle( + color: Colors.white, + fontSize: 18 * scaleFactor, + ), + ), + // child: Text( + // '${latte.order.name!}' + // '${latte.metadata.isNameApproved == false ? ' ❌' : ''}', + // style: TextStyle( + // color: Colors.white, + // fontSize: 18 * scaleFactor, + // ), + // ), + ), + ), + if (latte.metadata.isImageApproved == false) + Positioned( + top: 0, + right: 0, + left: 0, + child: Container( + padding: EdgeInsets.all(8 * scaleFactor), + color: Colors.red.withValues(alpha: 0.5), + child: Text( + 'Image rejected', + style: TextStyle( + color: Colors.white, + fontSize: 14 * scaleFactor, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/widgets/internal_order_queues.dart b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/internal_order_queues.dart new file mode 100644 index 0000000..0a85ff2 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/internal_order_queues.dart @@ -0,0 +1,235 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/role.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/barista/widgets/widgets.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Renders cards for all orders ready for either moderation or brewing. +/// +/// Baristas only see orders ready for brewing, but moderators see both queues +/// to better manage the entire latte bar. +class InternalOrderQueues extends StatelessWidget { + /// Instantiates a [InternalOrderQueues] widget. + const InternalOrderQueues._({ + required this.activeBarista, + required this.canClaimOrders, + required this.baristas, + required this.orders, + required this.role, + + // Moderation callbacks. + required this.onApproveAll, + required this.onRejectName, + required this.onRejectImage, + required this.onRejectBoth, + + // Brewing callbacks. + required this.onClaimPressed, + required this.onCompletePressed, + required this.onReprintPressed, + super.key, + }); + + /// Instantiates a [InternalOrderQueues] widget for a barista. + factory InternalOrderQueues.barista({ + required Barista activeBarista, + required bool canClaimOrders, + required Map baristas, + required List orders, + required void Function(String) onClaimPressed, + required void Function(String) onCompletePressed, + required void Function(String) onReprintPressed, + Key? key, + }) => InternalOrderQueues._( + activeBarista: activeBarista, + baristas: baristas, + canClaimOrders: canClaimOrders, + orders: orders, + role: .barista, + onApproveAll: null, + onRejectName: null, + onRejectImage: null, + onRejectBoth: null, + onClaimPressed: onClaimPressed, + onCompletePressed: onCompletePressed, + onReprintPressed: onReprintPressed, + key: key, + ); + + /// Instantiates a [InternalOrderQueues] widget for a barista. + factory InternalOrderQueues.moderator({ + required Map baristas, + required List orders, + required void Function(String) onApproveAll, + required void Function(String) onRejectName, + required void Function(String) onRejectImage, + required void Function(String) onRejectBoth, + required void Function(String) onCompletePressed, + Key? key, + }) => InternalOrderQueues._( + activeBarista: null, + canClaimOrders: false, + baristas: baristas, + orders: orders, + role: .moderator, + onApproveAll: onApproveAll, + onRejectName: onRejectName, + onRejectImage: onRejectImage, + onRejectBoth: onRejectBoth, + onClaimPressed: null, + onCompletePressed: onCompletePressed, + onReprintPressed: null, + key: key, + ); + + /// Non-null if the user is a barista. + final Barista? activeBarista; + + /// Whether the barista can claim orders. If this is false, the "Claim" button + /// should not be rendered. + /// + /// Irrelevant for moderators, who can never claim orders. + final bool canClaimOrders; + + /// All baristas, used to look up barista info for brew queue. + final Map baristas; + + /// The role of the user viewing the queues. Must be either [Role.barista] or + /// [Role.moderator]. + final Role role; + + /// Orders ready to be displayed. + final List orders; + + /// The barista has approved both the name and image for an order. + final void Function(String)? onApproveAll; + + /// The barista has rejected the name for an order. + final void Function(String)? onRejectName; + + /// The barista has rejected the image for an order. + final void Function(String)? onRejectImage; + + /// The barista has rejected both the name and image for an order. + final void Function(String)? onRejectBoth; + + /// The barista has claimed an order. + final void Function(String)? onClaimPressed; + + /// The barista has completed an order. + final void Function(String) onCompletePressed; + + /// The barista has requested to reprint an order. + final void Function(String)? onReprintPressed; + + static const _whiteText = TextStyle(color: AppColors.white); + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, layoutInfo) { + bool hasShownBrewLable = false; + bool hasShownModerationLabel = false; + + Widget getModerationQueueLabelOnce() { + final label = SizedBox( + height: (layoutInfo.height * 0.1).clamp(50, 100), + child: !hasShownModerationLabel + ? Center( + child: const Text( + 'Moderation Queue', + style: InternalOrderQueues._whiteText, + ).h3, + ) + : null, + ); + hasShownModerationLabel = true; + + return label; + } + + Widget getBrewQueueLabelOnce() { + final label = SizedBox( + height: (layoutInfo.height * 0.1).clamp(50, 100), + child: !hasShownBrewLable + ? Center( + child: const Text( + 'Brew Queue', + style: InternalOrderQueues._whiteText, + ).h3, + ) + : null, + ); + hasShownBrewLable = true; + + return label; + } + + return Padding( + padding: EdgeInsets.symmetric(horizontal: layoutInfo.width * 0.05), + child: ListView.separated( + scrollDirection: .horizontal, + itemBuilder: (context, index) { + final latte = orders[index]; + return Column( + crossAxisAlignment: .start, + children: [ + if (latte.metadata.status == LatteOrderStatus.submitted) + getModerationQueueLabelOnce(), + if (latte.metadata.status != LatteOrderStatus.submitted) + getBrewQueueLabelOnce(), + Expanded( + child: + role.isBarista || + latte.metadata.status != LatteOrderStatus.submitted + ? LatteOrderCard.barista( + activeBarista: activeBarista, + canClaimOrders: canClaimOrders, + claimedBy: latte.metadata.baristaId != null + ? baristas[latte.metadata.baristaId] + : null, + latte: latte, + onApproveAll: onApproveAll, + onRejectName: onRejectName, + onRejectImage: onRejectImage, + onRejectBoth: onRejectBoth, + onClaimPressed: onClaimPressed, + onCompletePressed: onCompletePressed, + onReprintPressed: onReprintPressed, + role: role, + key: ValueKey( + '${latte.order.id}--${latte.metadata.status}', + ), + ) + : LatteOrderCard.moderator( + latte: latte, + claimedBy: latte.metadata.baristaId != null + ? baristas[latte.metadata.baristaId] + : null, + onApproveAll: onApproveAll!, + onRejectName: onRejectName!, + onRejectImage: onRejectImage!, + onRejectBoth: onRejectBoth!, + onCompletePressed: onCompletePressed, + key: ValueKey( + '${latte.order.id}--${latte.metadata.status}', + ), + ), + ), + const SizedBox(height: 16), + ], + ); + }, + separatorBuilder: (context, index) => const SizedBox(width: 16), + itemCount: orders.length, + ), + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/widgets/latte_order_card.dart b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/latte_order_card.dart new file mode 100644 index 0000000..5678056 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/latte_order_card.dart @@ -0,0 +1,864 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' show min, pi; + +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter/material.dart' + show ElevatedButton, IconButton, OutlinedButton; +import 'package:genlatte/src/role.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/barista/widgets/widgets.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' hide IconButton; +import 'package:text_responsive/text_responsive.dart'; + +/// A card representing a single latte order in the queue. +class LatteOrderCard extends StatefulWidget { + /// Instantiates a [LatteOrderCard]. + const LatteOrderCard._({ + required this.activeBarista, + required this.canClaimOrders, + required this.claimedBy, + required this.latte, + required this.role, + + // Moderation callbacks + required this.onApproveAll, + required this.onRejectName, + required this.onRejectImage, + required this.onRejectBoth, + + // Brewing callbacks + required this.onClaimPressed, + required this.onCompletePressed, + required this.onReprintPressed, + + super.key, + }); + + /// Instantiates a [LatteOrderCard] for a barista. + /// + /// [Role] is passed to the barista view of the card because moderators can + /// see all submitted-but-not-completed orders, meaning some moderators will + /// view the barista side of the card; and [role] represents the user's role; + /// not the side of the card currently being shown. + /// + /// This is also why [onClaimPressed] and [onCompletePressed] are nullable; + /// they are not operations that moderators can perform. + factory LatteOrderCard.barista({ + required Barista? activeBarista, + required bool canClaimOrders, + required Barista? claimedBy, + required Latte latte, + required Role role, + required void Function(String)? onApproveAll, + required void Function(String)? onRejectName, + required void Function(String)? onRejectImage, + required void Function(String)? onRejectBoth, + required void Function(String)? onClaimPressed, + required void Function(String) onCompletePressed, + required void Function(String)? onReprintPressed, + Key? key, + }) => LatteOrderCard._( + activeBarista: activeBarista, + canClaimOrders: canClaimOrders, + claimedBy: claimedBy, + latte: latte, + role: role, + onApproveAll: onApproveAll, + onRejectName: onRejectName, + onRejectImage: onRejectImage, + onRejectBoth: onRejectBoth, + onClaimPressed: onClaimPressed, + onCompletePressed: onCompletePressed, + onReprintPressed: onReprintPressed, + key: key, + ); + + /// Instantiates a [LatteOrderCard] for a moderator. + /// + /// The moderator view of a card is more straightforward than its barista + /// counterpart because only moderators ever view cards in the `.submitted` + /// state (which could have been called "needsModeration"). + factory LatteOrderCard.moderator({ + required Latte latte, + required Barista? claimedBy, + required void Function(String) onApproveAll, + required void Function(String) onRejectName, + required void Function(String) onRejectImage, + required void Function(String) onRejectBoth, + required void Function(String) onCompletePressed, + Key? key, + }) => LatteOrderCard._( + activeBarista: null, + canClaimOrders: false, + claimedBy: claimedBy, + latte: latte, + role: .moderator, + onApproveAll: onApproveAll, + onRejectName: onRejectName, + onRejectImage: onRejectImage, + onRejectBoth: onRejectBoth, + onClaimPressed: null, + onCompletePressed: onCompletePressed, + onReprintPressed: null, + key: key, + ); + + /// The order to display. + final Latte latte; + + /// The role of the user viewing the card. Note that this is potentially + /// separate from which side of the card is being displayed; specifically in + /// the instance of a moderator viewing the barista side of the card. + final Role role; + + /// The barista who has claimed the order, if the order is that far along. + final Barista? claimedBy; + + /// Non-null if the user is a barista. + final Barista? activeBarista; + + /// Whether the active barista can claim orders. + /// + /// Irrelevant for moderators. + final bool canClaimOrders; + + /// MODERATION CALLBACKS /// + + /// Callback for when the user approves all. + /// + /// Guaranteed to be non-null when [role] == .moderator. + final void Function(String)? onApproveAll; + + /// Callback for when the user rejects the name. + /// + /// Guaranteed to be non-null when [role] == .moderator. + final void Function(String)? onRejectName; + + /// Callback for when the user rejects the image. + /// + /// Guaranteed to be non-null when [role] == .moderator. + final void Function(String)? onRejectImage; + + /// Callback for when the user rejects both. + /// + /// Guaranteed to be non-null when [role] == .moderator. + final void Function(String)? onRejectBoth; + + /// END MODERATION CALLBACKS /// + + /// + /// + + /// BREWING CALLBACKS /// + + /// Callback for when the claim button is pressed. + final void Function(String)? onClaimPressed; + + /// Callback for when the complete button is pressed. + final void Function(String) onCompletePressed; + + /// Callback for when the reprint button is pressed. + final void Function(String)? onReprintPressed; + + /// END BREWING CALLBACKS /// + + @override + State createState() => _LatteOrderCardState(); +} + +class _LatteOrderCardState extends State + with SingleTickerProviderStateMixin { + bool _isFlipped = false; + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + value: needsModeration ? 0.0 : 1.0, + ); + } + + bool get needsModeration => widget.latte.metadata.status == .submitted; + + void _toggleCard() { + if (_isFlipped) { + _controller.forward().ignore(); + } else { + _controller.reverse().ignore(); + } + setState(() { + _isFlipped = !_isFlipped; + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + // Calculate the rotation value (0 to pi) + final double rotationValue = _controller.value * pi; + + // Determine which side to show based on the 50% (pi/2) threshold. + // + // Note that this is different from `needsModeration`, which is a fact + // about the state of an order, not a fact about the UI showing that + // order. + final bool showingModerationSide = rotationValue < (pi / 2); + + assert( + () { + if (showingModerationSide) { + if (widget.onApproveAll == null || + widget.onRejectName == null || + widget.onRejectImage == null || + widget.onRejectBoth == null) { + throw Exception( + 'Moderation callbacks are required for moderation side', + ); + } + } + + // If we are not on the moderation side, we are of course on the + // brewing side. But, brewing callbacks are not required because + // moderators have read-only access to the brew queue to gain a full + // picture of the latte bar without actually fulfilling orders + // directly. + + return true; + }(), + 'Attempted to show Moderator side of card from Barista view', + ); + + return Transform( + transform: Matrix4.identity() + ..setEntry(3, 2, 0.001) // Adds perspective/depth + ..rotateY(rotationValue), + alignment: Alignment.center, + // If we are on the back side, we must flip the content + // horizontally so it isn't mirrored. + child: showingModerationSide + ? _LatteOrderCardInner.moderate( + latte: widget.latte, + onApproveAll: () => + widget.onApproveAll!(widget.latte.order.id!), + onRejectName: () => + widget.onRejectName!(widget.latte.order.id!), + onRejectImage: () => + widget.onRejectImage!(widget.latte.order.id!), + onRejectBoth: () => + widget.onRejectBoth!(widget.latte.order.id!), + // Cards *requiring* moderation cannot be flipped to their + // brewing side, as brewing is not available until *after* + // moderation is completed. + onComplete: () => + widget.onCompletePressed(widget.latte.order.id!), + onFlipPressed: widget.role.isModerator ? _toggleCard : null, + ) + : Transform( + alignment: Alignment.center, + transform: Matrix4.identity()..rotateY(pi), + child: _LatteOrderCardInner.brew( + activeBarista: widget.activeBarista, + canClaimOrders: widget.canClaimOrders, + claimedBy: widget.claimedBy, + latte: widget.latte, + onClaimPressed: widget.onClaimPressed != null + ? () => widget.onClaimPressed!(widget.latte.order.id!) + : null, + onCompletePressed: () => + widget.onCompletePressed(widget.latte.order.id!), + onReprintPressed: widget.onReprintPressed != null + ? () => widget.onReprintPressed!(widget.latte.order.id!) + : null, + // Conversely, cards *not* requiring moderation can always + role: widget.role, + // be flipped to their moderation side to change past + // be flipped to their moderation side to change past + // decisions, and then of course back again. + onFlipPressed: widget.role.isModerator ? _toggleCard : null, + ), + ), + ); + }, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} + +class _LatteOrderCardInner extends StatelessWidget { + const _LatteOrderCardInner._({ + required this.activeBarista, + required this.canClaimOrders, + required this.claimedBy, + required this.latte, + required this.onApproveAll, + required this.onRejectName, + required this.onRejectImage, + required this.onRejectBoth, + required this.onClaimPressed, + required this.onCompletePressed, + required this.onReprintPressed, + required this.onFlipPressed, + required this.role, + }); + + /// Creates the moderation side of the card. + factory _LatteOrderCardInner.moderate({ + required Latte latte, + required VoidCallback onApproveAll, + required VoidCallback onRejectName, + required VoidCallback onRejectImage, + required VoidCallback onRejectBoth, + required VoidCallback? onFlipPressed, + required VoidCallback onComplete, + }) => _LatteOrderCardInner._( + activeBarista: null, + canClaimOrders: false, + claimedBy: null, + latte: latte, + onApproveAll: onApproveAll, + onRejectName: onRejectName, + onRejectImage: onRejectImage, + onRejectBoth: onRejectBoth, + onClaimPressed: null, + onCompletePressed: onComplete, + onReprintPressed: null, + onFlipPressed: onFlipPressed, + role: .moderator, + ); + + /// Creates the brewing side of the card. + factory _LatteOrderCardInner.brew({ + required Barista? activeBarista, + required bool canClaimOrders, + required Barista? claimedBy, + required Latte latte, + required VoidCallback? onClaimPressed, + required VoidCallback onCompletePressed, + required VoidCallback? onReprintPressed, + required VoidCallback? onFlipPressed, + required Role role, + }) => _LatteOrderCardInner._( + activeBarista: activeBarista, + canClaimOrders: canClaimOrders, + claimedBy: claimedBy, + latte: latte, + onApproveAll: null, + onRejectName: null, + onRejectImage: null, + onRejectBoth: null, + onClaimPressed: onClaimPressed, + onCompletePressed: onCompletePressed, + onReprintPressed: onReprintPressed, + onFlipPressed: onFlipPressed, + role: role, + ); + + final Latte latte; + final Barista? claimedBy; + final bool canClaimOrders; + + /// Non-null if the user is a barista. + final Barista? activeBarista; + + // Moderation callbacks. + + final VoidCallback? onApproveAll; + final VoidCallback? onRejectName; + final VoidCallback? onRejectImage; + final VoidCallback? onRejectBoth; + + // Brewing callbacks. + + final VoidCallback? onClaimPressed; + final VoidCallback onCompletePressed; + final VoidCallback? onReprintPressed; + final Role role; + + // UI callbacks. + + final VoidCallback? onFlipPressed; + + bool get _isActive => + _isMe && latte.metadata.status == LatteOrderStatus.inProgress; + + bool get _isMe => + activeBarista != null && + claimedBy != null && + activeBarista?.id == claimedBy?.id; + + /// Arbitrary numbers picked by fair die roll. + static const _idealSize = Size(230, 420); + + @override + Widget build(BuildContext context) { + return ResponsiveSizedBox.builder( + aspectRatioClamp: (null, _idealSize.aspectRatio, null), + builder: (context, size) { + final heightScaleFactor = size.height / _idealSize.height; + final scaleFactor = min( + size.width / _idealSize.width, + heightScaleFactor, + ); + + final padding = 20 * scaleFactor; + + final bool isActive = _isActive; + + return Container( + width: size.width, + height: size.height, + padding: EdgeInsets.all(padding), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16 * scaleFactor), + border: isActive + ? Border.all( + color: const Color(0xFF3B82F6), + width: 8 * scaleFactor, + ) + : null, + boxShadow: [ + BoxShadow( + color: isActive + ? const Color(0xFF3B82F6).withValues(alpha: 0.25) + : Colors.black.withValues(alpha: 0.08), + blurRadius: isActive ? 40 * scaleFactor : 25 * scaleFactor, + spreadRadius: isActive ? 4 * scaleFactor : 0, + offset: Offset(0, 10 * scaleFactor), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: .stretch, + children: [ + // Context Header + Expanded( + child: Row( + mainAxisAlignment: .spaceBetween, + children: [ + Text( + 'Order #${latte.metadata.orderNumber}', + style: TextStyle( + fontSize: 11 * scaleFactor, + color: const Color(0xFF444444), + fontWeight: FontWeight.w500, + ), + ), + HumanizedCountdown( + label: 'Submitted', + timestamp: latte.metadata.orderSubmittedTime!, + textStyle: TextStyle( + fontSize: 11 * scaleFactor, + color: const Color(0xFF444444), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + + Expanded( + flex: 3, + child: ImageStack(latte: latte, scaleFactor: scaleFactor), + ), + + SizedBox(height: 16 * scaleFactor), + + // Action Buttons + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: .stretch, + children: onApproveAll != null + ? _buildModerationButtons( + scaleFactor, + heightScaleFactor, + ) + : _buildBrewButtons(scaleFactor, canClaimOrders), + ), + ), + Expanded( + child: Row( + children: [ + Expanded( + flex: 5, + child: kDebugMode || role == .moderator + ? ClipboardValue( + '${latte.order.id}', + label: 'Order Id', + ) + : const SizedBox.shrink(), + ), + const Spacer(), + if (onFlipPressed != null && + // Don't show flip button on submitted orders. Order + // cards are only flippable *after* moderation is + // complete. + latte.metadata.status != .submitted) + IconButton( + onPressed: onFlipPressed, + icon: const Icon(Icons.flip), + ), + if (onFlipPressed == null) // + const SizedBox(height: 50), + ], + ), + ), + ], + ), + ); + }, + ); + } + + List _buildBrewButtons(double scaleFactor, bool canClaimOrders) { + return [ + Row( + children: [ + Expanded(child: _buildStatusBadge(scaleFactor)), + const SizedBox(width: 8), + Expanded( + child: _buildButtonOrAssigneBanner(scaleFactor, canClaimOrders), + ), + ], + ), + SizedBox(height: 16 * scaleFactor), + OrderDetailsWidget( + scaleFactor: scaleFactor, + details: [ + OrderDetailItem( + icon: const Icon(Icons.local_drink), + label: 'Milk:', + value: latte.order.milk!, + ), + OrderDetailItem( + icon: const Icon(Icons.cookie), + label: 'Sweetener:', + value: latte.order.sweetener!, + ), + + /// Always the same for Cloud Next, but will vary at I/O and beyond. + OrderDetailItem( + icon: const Icon(Icons.coffee), + label: 'Drink type:', + value: 'Latte', + ), + ], + ), + SizedBox(height: 16 * scaleFactor), + ]; + } + + List _buildModerationButtons( + double scaleFactor, + double heightScaleFactor, + ) => [ + _buildModerateButton( + label: '✓ Approve All', + shortcut: 'A', + backgroundColor: const Color(0xFF10B981), + textColor: Colors.white, + shortcutColor: Colors.white.withValues(alpha: 0.3), + onPressed: onApproveAll!, + scaleFactor: scaleFactor, + heightScaleFactor: heightScaleFactor, + ), + SizedBox(height: 12 * scaleFactor), + Row( + children: [ + Expanded( + child: _buildModerateButton( + label: '⚠️ Reject Name', + shortcut: 'N', + backgroundColor: const Color(0xFFFFFBEB), + textColor: const Color(0xFFD97706), + borderColor: const Color(0xFFFCD34D), + shortcutColor: Colors.black.withValues( + alpha: 0.05, + ), + onPressed: onRejectName!, + scaleFactor: scaleFactor, + heightScaleFactor: heightScaleFactor, + ), + ), + SizedBox(width: 12 * scaleFactor), + Expanded( + child: _buildModerateButton( + label: '⚠️ Reject Image', + shortcut: 'I', + backgroundColor: const Color(0xFFFFFBEB), + textColor: const Color(0xFFD97706), + borderColor: const Color(0xFFFCD34D), + shortcutColor: Colors.black.withValues( + alpha: 0.05, + ), + onPressed: onRejectImage!, + scaleFactor: scaleFactor, + heightScaleFactor: heightScaleFactor, + ), + ), + ], + ), + SizedBox(height: 12 * scaleFactor), + _buildModerateButton( + label: '❌ Reject Both', + shortcut: 'R', + backgroundColor: const Color(0xFFFEE2E2), + textColor: const Color(0xFFDC2626), + borderColor: const Color(0xFFFECACA), + shortcutColor: Colors.black.withValues(alpha: 0.05), + onPressed: onRejectBoth!, + scaleFactor: scaleFactor, + heightScaleFactor: heightScaleFactor, + ), + ]; + + Widget _buildButtonOrAssigneBanner(double scaleFactor, bool canClaimOrders) { + if (claimedBy == null && onClaimPressed != null) { + if (!canClaimOrders) { + return const Text( + 'Choose a Printer', + textAlign: .end, + style: TextStyle(color: AppColors.googleIntroRed), + ).bold; + } + return ElevatedButton( + onPressed: onClaimPressed, + style: ElevatedButton.styleFrom( + // minimumSize: Size.zero, + backgroundColor: const Color(0xFF3B82F6), + foregroundColor: Colors.white, + // elevation: 0, + padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + 'Claim Order', + style: TextStyle( + fontSize: 12 * scaleFactor, + ), + ), + ); + } else if (_isMe) { + return ElevatedButton( + onPressed: onCompletePressed, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF10B981), + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + 'Complete', + style: TextStyle( + fontSize: 12 * scaleFactor, + ), + ), + ); + } else if (role.isModerator && + latte.metadata.status == LatteOrderStatus.inProgress) { + return ElevatedButton( + onPressed: onCompletePressed, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF10B981), + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + 'Complete', + style: TextStyle( + fontSize: 12 * scaleFactor, + ), + ), + ); + } else { + return const SizedBox.shrink(); + } + } + + Widget _buildStatusBadge(double scaleFactor) { + if (_isActive && onReprintPressed != null) { + return _ReprintButton( + onReprintPressed: onReprintPressed!, + scaleFactor: scaleFactor, + ); + } else if (_isActive) { + return const SizedBox.shrink(); + } + + return Container( + padding: EdgeInsets.symmetric( + horizontal: 14 * scaleFactor, + vertical: 4 * scaleFactor, + ), + decoration: BoxDecoration( + color: claimedBy != null + ? const Color(0xFFF3E8FF) + : const Color(0xFFE0F2FE), + borderRadius: BorderRadius.circular(20 * scaleFactor), + ), + child: ParagraphTextWidget( + claimedBy != null ? claimedBy!.username : 'PENDING', + maxLines: 1, + textAlign: .center, + style: TextStyle( + fontSize: 10 * scaleFactor, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + color: claimedBy != null + ? const Color(0xFF7E22CE) + : const Color(0xFF0369A1), + ), + ), + ); + } + + Widget _buildAssigneeBanner(double scaleFactor) { + // Claimed State Banner + return Container( + padding: EdgeInsets.symmetric( + horizontal: 3 * scaleFactor, + vertical: 3 * scaleFactor, + ), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(8 * scaleFactor), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: Center( + child: ParagraphTextWidget( + claimedBy!.username, + style: const TextStyle(fontWeight: FontWeight.bold), + maxLines: 1, + ), + ), + ); + } + + // Helper method to keep button styling consistent and code DRY + Widget _buildModerateButton({ + required String label, + required String shortcut, + required Color backgroundColor, + required Color textColor, + required Color shortcutColor, + required VoidCallback onPressed, + required double scaleFactor, + required double heightScaleFactor, + Color? borderColor, + }) { + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + minimumSize: Size.zero, + backgroundColor: backgroundColor, + foregroundColor: textColor, + elevation: 0, + padding: EdgeInsets.symmetric(vertical: 12 * heightScaleFactor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8 * scaleFactor), + side: borderColor != null + ? BorderSide(color: borderColor) + : BorderSide.none, + ), + ), + child: Row( + mainAxisAlignment: .center, + children: [ + Text( + label, + style: TextStyle( + fontSize: 10 * scaleFactor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} + +class _ReprintButton extends StatefulWidget { + const _ReprintButton({ + required this.onReprintPressed, + required this.scaleFactor, + }); + + final VoidCallback onReprintPressed; + final double scaleFactor; + + @override + State<_ReprintButton> createState() => _ReprintButtonState(); +} + +class _ReprintButtonState extends State<_ReprintButton> { + bool _isPrinting = false; + + Future _handlePress() async { + if (_isPrinting) return; + + setState(() => _isPrinting = true); + widget.onReprintPressed(); + + await Future.delayed(const Duration(seconds: 3)); + + if (mounted) { + setState(() => _isPrinting = false); + } + } + + @override + Widget build(BuildContext context) { + return OutlinedButton( + onPressed: _isPrinting ? null : _handlePress, + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF3B82F6), + disabledForegroundColor: const Color(0xFF3B82F6).withValues(alpha: 0.5), + side: BorderSide( + color: _isPrinting + ? const Color(0xFF3B82F6).withValues(alpha: 0.5) + : const Color(0xFF3B82F6), + ), + padding: EdgeInsets.symmetric(vertical: 6 * widget.scaleFactor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: _isPrinting + ? SizedBox( + width: 14 * widget.scaleFactor, + height: 14 * widget.scaleFactor, + child: const CircularProgressIndicator(), + ) + : Text( + '⬆️ Printer', + style: TextStyle( + fontSize: 10 * widget.scaleFactor, + ), + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/widgets/order_details_table.dart b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/order_details_table.dart new file mode 100644 index 0000000..07d30ac --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/order_details_table.dart @@ -0,0 +1,122 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:text_responsive/text_responsive.dart'; + +/// Simple model for the data to display. +class OrderDetailItem { + /// Instantiates a [OrderDetailItem]. + OrderDetailItem({ + required this.icon, + required this.label, + required this.value, + }); + + /// Leading visual indicator of what the row is about. + final Widget icon; + + /// Plain text label. + final String label; + + /// Configuration value for the label. + final String value; +} + +/// Displays a list of [OrderDetailItem]s in a table. +class OrderDetailsWidget extends StatelessWidget { + /// Instantiates an [OrderDetailsWidget]. + const OrderDetailsWidget({ + required this.details, + this.scaleFactor = 1.0, + super.key, + }); + + /// The details to display. + final List details; + + /// The factor by which to scale the UI. + final double scaleFactor; + + @override + Widget build(BuildContext context) { + if (details.isEmpty) return const SizedBox.shrink(); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12 * scaleFactor), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.03), + blurRadius: 4 * scaleFactor, + offset: Offset(0, 2 * scaleFactor), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, // Hugs the content tightly + children: details.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + final isLast = index == details.length - 1; + + return Column( + children: [ + Padding( + padding: EdgeInsets.all(10 * scaleFactor), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Left Side: Icon and Label + Row( + children: [ + SizedBox( + width: 24 * scaleFactor, + child: Center( + child: Transform.scale( + scale: scaleFactor, + child: item.icon, + ), + ), + ), + SizedBox(width: 12 * scaleFactor), + ParagraphTextWidget( + item.label, + style: TextStyle( + fontSize: 12 * scaleFactor, + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + ), + ], + ), + + // Right Side: Value + ParagraphTextWidget( + item.value, + style: TextStyle( + fontSize: 16 * scaleFactor, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + ), + // Smart Divider: Only show if it's NOT the last item + if (!isLast) + Divider( + height: 1 * scaleFactor, + thickness: 1 * scaleFactor, + color: Colors.grey.shade200, + ), + ], + ); + }).toList(), + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/barista/widgets/widgets.dart b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/widgets.dart new file mode 100644 index 0000000..71d8dcd --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/barista/widgets/widgets.dart @@ -0,0 +1,10 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'barista_persona_card.dart'; +export 'humanized_countdown.dart'; +export 'image_stack.dart'; +export 'internal_order_queues.dart'; +export 'latte_order_card.dart'; +export 'order_details_table.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/kiosk/CONTEXT.md new file mode 100644 index 0000000..04df99c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/CONTEXT.md @@ -0,0 +1,11 @@ +# Kiosk / Barista / Moderator / Queue / Recent Orders Roots + +**Purpose:** +This directory acts as the architectural boundary grouping for the application's distinct personas. + +**Implementation Details:** +- **Barrel Architecture**: This level strictly exports child module directories like `home/` and `widgets/` via an `index` / `barrel` file. +- **Child Contexts**: Please navigate into `home/` or `widgets/` subdirectories to view specific BLoC architectures, View layouts, and complex logic files tailored towards each app persona. + +**Dependencies:** +- Consumed by `core/routing/routes.dart` to map physical URLs to specific screen entry points. diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/CONTEXT.md new file mode 100644 index 0000000..25b9d58 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/CONTEXT.md @@ -0,0 +1,25 @@ +# Kiosk Home + +**Purpose:** +Manages the application state, wizard-style navigation, and primary user interface for the consumer-facing Kiosk app. It orchestrates the entire order flow—from entering a name and selecting drink options to submitting a "happy place" and interacting with generated AI imagery. + +**Detailed File Overviews:** + +- `kiosk_home.dart`: + - **Description**: A barrel file. + - **Usage/Exports**: Re-exports the Bloc (`kiosk_home_bloc.dart`) and the View (`kiosk_home_view.dart`). + +- `kiosk_home_bloc.dart`: + - **Description**: The massive, central state management engine for the Kiosk screen wizard. + - **Core Logic**: Manages a complex step-by-step wizard state (`KioskWizardStep` enum), handling forward/backward navigation and validations. It coordinates heavy asynchronous tasks like creating a `LatteOrder` in Firestore, watching for image generation streams (`_watchImageBatch`), processing moderation events (e.g. `HappyPlaceRejected`), submitting AI image tweaks via follow-up questions, and ultimately finalizing an order via `_onSubmitOrder`. + - **Exposed Methods**: Consumes numerous `KioskHomeEvent` variants (e.g., `StartOver`, `SubmitHappyPlace`, `GenerateRevisedImages`, `AcceptImage`). Emits updated `KioskHomeState` objects representing the user's progress through the ordering flow. + +- `kiosk_home_view.dart`: + - **Description**: The UI orchestration layer for the Kiosk. + - **Core Logic**: Wraps the wizard in a `ZipPageView`, binding `KioskHomeBloc` states directly to individual wizard sub-screens (e.g., `KioskDrinkOrderName`, `KioskHappyPlace`, `KioskTweakImage`) located in the `/steps` subdirectory. Also manages top-level error toasts (like happy place moderation rejections) and layout structure (headers/footers, segmented progress bar). + - **Dependencies**: Imports and renders all specific step components from `kiosk/home/steps/steps.dart`. + +**Dependencies/Relationships:** +- Heaps of interaction with `genlatte_data` (e.g. `LatteOrdersRepository`, `Repository`). +- Directly delegates to individual UI fragments within the `steps/` subfolder. +- Consumes components from `kiosk/widgets/` like `SegmentedProgress`, `ZipPageView`. diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home.dart new file mode 100644 index 0000000..0d7f300 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'kiosk_home_bloc.dart'; +export 'kiosk_home_view.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home_bloc.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home_bloc.dart new file mode 100644 index 0000000..abe430f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home_bloc.dart @@ -0,0 +1,1094 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: use_if_null_to_convert_nulls_to_bools + +import 'dart:async'; + +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte/src/data/data.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; +import 'package:uuid/uuid.dart'; + +part 'kiosk_home_bloc.freezed.dart'; + +final _logger = Logger('KioskHomeBloc'); + +typedef _Emit = Emitter; + +/// State manager for the Kiosk home page. +class KioskHomeBloc extends Bloc { + /// {@macro KioskHomeBloc} + KioskHomeBloc() : super(KioskHomeState.initial()) { + on((event, _Emit emit) { + _logger.finer('Kiosk event: $event'); + return switch (event) { + OnNewPage() => _onNewPage(event, emit), + StartOver() => _onStartOver(event, emit), + _EjectData() => _onEjectData(event, emit), + LoadPreExistingOrders() => _onLoadPreExistingOrders(event, emit), + ApplyPreExistingOrder() => _onApplyPreExistingOrder(event, emit), + UpdateMilkOptions() => _onUpdateMilkOptions(event, emit), + UpdateSweetenerOptions() => _onUpdateSweetenerOptions(event, emit), + GoBackKioskWizard() => _onGoBackKioskWizard(event, emit), + GoToStep() => _onGoToStep(event, emit), + SubmitUserName() => _onSubmitUserName(event, emit), + SelectMilk() => _onSelectMilk(event, emit), + SelectSweetener() => _onSelectSweetener(event, emit), + SubmitMilkAndSweetener() => _onSubmitMilkAndSweetener(event, emit), + SubmitHappyPlace() => _onSubmitHappyPlace(event, emit), + HappyPlaceRejected() => _onHappyPlaceRejected(event, emit), + HappyPlaceModerationReasonShown() => _onHappyPlaceModerationReasonShown( + event, + emit, + ), + ServerOrderUpdate() => _onServerOrderUpdate(event, emit), + ServerMetadataUpdate() => _onServerMetadataUpdate(event, emit), + UpdateLatteImages() => _onUpdateLatteImages(event, emit), + NewImageLatteBatch() => _onNewImageLatteBatch(event, emit), + SelectImage() => _onSelectImage(event, emit), + UpdateQuestions() => _onUpdateQuestions(event, emit), + AnswerQuestion() => _onAnswerQuestion(event, emit), + GenerateRevisedImages() => _onGenerateRevisedImages(event, emit), + RejectImageBatch() => _onRejectImageBatch(event, emit), + AcceptImage() => _onAcceptImage(event, emit), + SubmitOrder() => _onSubmitOrder(event, emit), + }; + }); + + _orderRepository = GetIt.I(); + _metadataRepository = GetIt.I>(); + _imagesRepository = GetIt.I>(); + _optionsRepository = GetIt.I>(); + + _orderRepository + .getItems( + details: RequestDetails.read( + requestType: .allLocal, + ), + ) + .then((orders) { + add(LoadPreExistingOrders(orders)); + }) + .ignore(); + + _loadForStep(state.currentStep, null); + _loadMilksAndSweeteners(); + } + + late final LatteOrdersRepository _orderRepository; + late final Repository _metadataRepository; + late final Repository _imagesRepository; + late final Repository _optionsRepository; + + StreamSubscription? _milkOptionsSubscription; + StreamSubscription? _sweetenerOptionsSubscription; + StreamSubscription? _orderSubscription; + StreamSubscription? _metadataSubscription; + StreamSubscription? _imageBatchSubscription; + + void _loadMilksAndSweeteners() { + final milkOptionsStream = _optionsRepository.watch('milks'); + final sweetenerOptionsStream = _optionsRepository.watch('sweeteners'); + + _milkOptionsSubscription ??= milkOptionsStream.listen( + (milkOptions) { + add(UpdateMilkOptions(milkOptions?.values)); + }, + ); + _sweetenerOptionsSubscription ??= sweetenerOptionsStream.listen( + (sweetenerOptions) { + add(UpdateSweetenerOptions(sweetenerOptions?.values)); + }, + ); + } + + Future _onStartOver(StartOver event, _Emit emit) async { + _logger.fine('_onStartOver() called'); + emit(state.copyWith(shouldClearData: true)); + _goToStep(.intro, emit); + } + + Future _onEjectData(_EjectData event, _Emit emit) async { + _logger.fine('_onEjectData() called'); + + // // Cancel any existing subscriptions + await _orderSubscription?.cancel(); + await _metadataSubscription?.cancel(); + await _imageBatchSubscription?.cancel(); + + // // Delete the order from local memory + await _orderRepository.delete( + state.order!.id!, + RequestDetails.write(requestType: .local), + ); + + // // Reset the state + emit( + KioskHomeState.initial().copyWith( + milkOptions: state.milkOptions, + sweetenerOptions: state.sweetenerOptions, + ), + ); + } + + Future _onLoadPreExistingOrders( + LoadPreExistingOrders event, + _Emit emit, + ) async { + _logger.fine('_onLoadPreExistingOrders() called'); + + if (event.orders.isEmpty) { + // No problem, but also nothing to do + return; + } + if (event.orders.length > 1) { + // There should only ever be one order at a time for the kiosk. + // If there are more than one, then something is wrong. + // If there are none, then we should start from the beginning. + final deleteFutures = >[]; + for (final order in event.orders) { + deleteFutures.add( + _orderRepository.delete( + order.id!, + RequestDetails.write(requestType: .local), + ), + ); + } + await Future.wait(deleteFutures); + return; + } + + final order = event.orders.first; + + // Load the metadata for the order; locally first, or from Firebase if we + // do not get a cache hit locally. + final metadata = await _metadataRepository.getById(order.id!); + + if (metadata == null) { + // A cached order survived. Un-good. + // Delete it and start over. + await _orderRepository.delete( + order.id!, + RequestDetails.write(requestType: .local), + ); + return; + } + add(ApplyPreExistingOrder(order, metadata)); + } + + Future _onApplyPreExistingOrder( + ApplyPreExistingOrder event, + _Emit emit, + ) async { + _logger.fine('_onApplyPreExistingOrder() called with order ${event.order}'); + + late KioskWizardStep stepToJumpTo; + + if (event.metadata.status == .submitted) { + add(const StartOver()); + return; + } + + _watchOrder(event.order); + + if (event.metadata.imageBatchId != null) { + _watchImageBatch(event.metadata.imageBatchId!); + } + + if (event.order.name == null) { + stepToJumpTo = .name; + } else if (event.order.milk == null || event.order.sweetener == null) { + stepToJumpTo = .milkAndSweetener; + } else if (event.order.happyPlace == null || + event.order.happyPlace!.isEmpty) { + stepToJumpTo = .happyPlace; + } else if (event.metadata.imageUrl != null) { + stepToJumpTo = .submitOrder; + } + // From here on down, the user could have fallen into odd cracks, like + // having submitted their happy place but not having received notice of the + // resulting [LatteImageBatch] + else { + if (event.metadata.imageBatchId == null) { + // Let the user re-submit this and resume. + stepToJumpTo = .happyPlace; + } else { + // We are already watching the image batch Id, but we also need to load + // that value immediately to figure out whether the user had made it all + // the way to image tweaking, or just original image selection. + final imageBatch = await _imagesRepository.getById( + event.metadata.imageBatchId!, + ); + if (imageBatch == null || imageBatch.parent == null) { + stepToJumpTo = .chooseAnImage; + } else { + stepToJumpTo = .chooseATweakedImage; + } + } + } + + emit( + state.copyWith( + order: event.order, + metadata: event.metadata, + currentStep: stepToJumpTo, + ), + ); + } + + void _onUpdateMilkOptions(UpdateMilkOptions event, _Emit emit) { + _logger.fine( + '_onUpdateMilkOptions() called ' + 'with [${event.milkOptions?.join(', ')}]', + ); + emit(state.copyWith(milkOptions: event.milkOptions)); + } + + void _onUpdateSweetenerOptions(UpdateSweetenerOptions event, _Emit emit) { + _logger.fine( + '_onUpdateSweetenerOptions() called ' + 'with [${event.sweetenerOptions?.join(', ')}]', + ); + emit(state.copyWith(sweetenerOptions: event.sweetenerOptions)); + } + + Future _onGoBackKioskWizard(GoBackKioskWizard event, _Emit emit) async { + _logger.fine( + '_onGoBackKioskWizard() called from step .${state.currentStep.name}', + ); + + // First, handle special cases; then fall back to default logic of + // navigating to the N-1 step. + + // First special case: going back from the Accept Order screen + if (state.currentStep == .submitOrder) { + final KioskWizardStep stepToGoBackTo = state.imagesBatch!.parent != null + // If the parent is set, then the person ultimately choose a + ? .chooseATweakedImage + : .chooseAnImage; + return add(GoToStep(stepToGoBackTo)); + } else if (state.currentStep == .chooseATweakedImage) { + // A special `isSubmitting` update to lock down the forward button while + // the server switches around the Order's image batch and we prepare to + // navigate backwards. This will be cleared upon arriving at the new + // wizard step. + emit(state.copyWith(isSubmitting: true)); + + // And now reject. + return add(const RejectImageBatch()); + } + + final previousStep = state.currentStep.previousStep; + if (previousStep == null) { + return; + } + add(GoToStep(previousStep)); + } + + Future _onGoToStep(GoToStep event, _Emit emit) async { + _logger.fine('Explicitly navigating to .${event.step.name}'); + _goToStep(event.step, emit); + } + + void _goToStep(KioskWizardStep step, _Emit emit) { + _logger.fine('_goToStep() called with .${step.name}'); + emit(state.copyWith(currentStep: step)); + _loadForStep(step, emit); + } + + Future _onSubmitUserName(SubmitUserName event, _Emit emit) async { + _logger.fine('_onSubmitUserName() called with name: "${event.name}"'); + + if (event.name.isEmpty) { + return; + } + + if (event.name == state.order?.name) { + _goToStep(.milkAndSweetener, emit); + return; + } + + emit(state.copyWith(isSubmitting: true)); + final order = + // The Order may already exist if moderation kicked the user back + // to this step. + state.order?.copyWith(name: event.name) ?? + // But if it does not already exist, which is the most common + // scenario, then create one. + LatteOrder(name: event.name); + + final savedOrder = await _orderRepository.setItem(order); + emit(state.copyWith(order: savedOrder)); + _watchOrder(savedOrder); + add(const GoToStep(.milkAndSweetener)); + } + + Future _onSelectMilk(SelectMilk event, _Emit emit) async { + _logger.fine('_onSelectMilk() called with milk: "${event.milk}"'); + assert(state.order != null, 'Order must exist when selecting milk'); + emit(state.copyWith(order: state.order!.copyWith(milk: event.milk))); + } + + Future _onSelectSweetener(SelectSweetener event, _Emit emit) async { + _logger.fine( + '_onSelectSweetener() called with sweetener: "${event.sweetener}"', + ); + assert(state.order != null, 'Order must exist when selecting sweetener'); + emit( + state.copyWith(order: state.order!.copyWith(sweetener: event.sweetener)), + ); + } + + Future _onSubmitMilkAndSweetener( + SubmitMilkAndSweetener event, + _Emit emit, + ) async { + _logger.fine('_onSubmitMilkAndSweetener() called'); + assert( + state.order != null, + 'Order must exist when submitting milk and sweetener', + ); + emit(state.copyWith(isSubmitting: true)); + final savedOrder = await _orderRepository.setItem(state.order!); + emit(state.copyWith(order: savedOrder)); + add(const GoToStep(.happyPlace)); + } + + Future _onSubmitHappyPlace(SubmitHappyPlace event, _Emit emit) async { + _logger.fine( + '_onSubmitHappyPlace() called with happy place: "${event.happyPlace}"', + ); + assert(state.order != null, 'Order must exist when submitting happy place'); + + // Check whether we're passing through again with an already-moderated + // happy place. + final isSameHappyPlace = + state.metadata!.isHappyPlaceApproved == true && + event.happyPlace.isNotEmpty && + state.order!.happyPlace == event.happyPlace; + + if (!isSameHappyPlace) { + emit(state.copyWith(isSubmitting: true)); + _imageBatchSubscription?.cancel().ignore(); + final order = state.order!.copyWith(happyPlace: event.happyPlace); + emit( + state.copyWith( + imagesBatch: null, + selectedImageIndex: null, + imageGenerationStartTime: DateTime.now(), + ), + ); + final savedOrder = await _orderRepository.setItem(order); + emit(state.copyWith(order: savedOrder)); + } + + return add(const GoToStep(.chooseAnImage)); + } + + void _onHappyPlaceRejected(HappyPlaceRejected event, _Emit emit) { + _logger.info('Happy place rejected for reason: "${event.reason}"'); + emit( + state.copyWith( + order: state.order?.copyWith(happyPlace: null), + happyPlaceModerationEvent: ModerationEvent(event.reason), + ), + ); + add(const GoToStep(.happyPlace)); + } + + void _onHappyPlaceModerationReasonShown( + HappyPlaceModerationReasonShown event, + _Emit emit, + ) { + _logger.finer('_onHappyPlaceModerationReasonShown() called'); + emit(state.copyWith(happyPlaceModerationEvent: null)); + } + + /// Opens a stream with Firebase to watch for changes to the [LatteOrder]. + void _watchOrder(LatteOrder? order) { + _logger.fine('_watchOrder() called for order ${order?.id}'); + _orderSubscription?.cancel().ignore(); + _metadataSubscription?.cancel().ignore(); + if (order != null) { + _logger.info('Watching Order Id ${order.id}'); + final orderStream = _orderRepository.watch(order.id!); + _orderSubscription = orderStream.listen(_onNewOrderFromServer); + + // Order and Metadata share the same Id + final metadataStream = _metadataRepository.watch(order.id!); + _metadataSubscription = metadataStream.listen(_onNewMetadataFromServer); + } + } + + Future _onNewOrderFromServer(LatteOrder? order) async { + LatteOrder? serverOrder = order; + _logger.finer('New LatteOrder from server: $serverOrder'); + + if (serverOrder != null && state.currentStep == .milkAndSweetener) { + // Catch a potential race condition where users progressing quickly from + // the .name step and into the .milkAndSweetener step and immediately + // setting a milk or sweetener value can sometimes have that value + // overridden by the server if the initial value from the LatteOrder watch + // stream is slow, and specifically, arrives after the user has already + // begun modifying local state. + // + // There are no other known scenarios where this race condition can occur + // because it is dependent on the relationship between initializing the + // stream and then modifying local state before the first payload, which + // is only known to be possible in this specific scenario. + if (state.order?.milk != null && serverOrder.milk == null) { + serverOrder = serverOrder.copyWith(milk: state.order!.milk); + } + if (state.order?.sweetener != null && serverOrder.sweetener == null) { + serverOrder = serverOrder.copyWith(sweetener: state.order!.sweetener); + } + } + + add(ServerOrderUpdate(serverOrder)); + if (order == null) { + _onNewMetadataFromServer(null).ignore(); + } + } + + /// Handler for when [LatteOrderMetadata] objects arrive from the server. + /// + /// This function applies fields expected to spontaneously change from the + /// server (typically moderation results). It does not meticulously examine + /// every field (like `questions`), which are expected to be set in advance + /// by the server and not randomly updated thereafter. + Future _onNewMetadataFromServer(LatteOrderMetadata? metadata) async { + _logger.finer('New LatteOrderMetadata from server: $metadata'); + if (metadata != null) { + if (metadata.imageBatchId != state.metadata?.imageBatchId) { + _watchImageBatch(metadata.imageBatchId!); + } + if (state.metadata != null) { + if (state.metadata!.isHappyPlaceApproved != true && + metadata.isHappyPlaceApproved == true) { + _logger.fine('Happy place APPROVED by moderation'); + } else if (state.metadata!.isHappyPlaceApproved != false && + metadata.isHappyPlaceApproved == false) { + _logger.fine('Happy place REJECTED by moderation'); + add( + HappyPlaceRejected( + metadata.happyPlaceModerationReason ?? + 'This value did not pass moderation.', + ), + ); + } else if (metadata.imageUrl != null && + state.metadata!.imageUrl != metadata.imageUrl) { + _logger.fine('Image URL has been accepted for Order ${metadata.id}'); + add(const GoToStep(.submitOrder)); + } else if (state.metadata!.status != .submitted && + metadata.status == .submitted) { + _logger.fine('Order ${metadata.id} submitted'); + add(const GoToStep(.confirmation)); + } + } + } + add(ServerMetadataUpdate(metadata)); + } + + void _watchImageBatch(String imageBatchId) { + _logger.fine('_watchImageBatch() called with imageBatchId=$imageBatchId'); + if (imageBatchId == state.imagesBatch?.id) { + _logger.fine('Already watching LatteImageBatch $imageBatchId'); + return; + } + _imageBatchSubscription?.cancel().ignore(); + _logger.fine('Watching ImageBatch Id: $imageBatchId'); + final imagesStream = _imagesRepository.watch(imageBatchId); + _imageBatchSubscription = imagesStream.listen((batch) { + add(UpdateLatteImages(batch)); + }); + + // Finally add the event for broader reactions to this change. + add(NewImageLatteBatch(imageBatchId)); + } + + void _onServerOrderUpdate(ServerOrderUpdate event, _Emit emit) { + _logger.finer( + '_onServerOrderUpdate() called with order.id=${event.order?.id}', + ); + if (event.order != null) { + emit(state.copyWith(order: event.order)); + } else { + add(const StartOver()); + } + } + + void _onServerMetadataUpdate(ServerMetadataUpdate event, _Emit emit) { + _logger.finer('_onServerMetadataUpdate() called'); + emit(state.copyWith(metadata: event.metadata)); + } + + void _onUpdateLatteImages(UpdateLatteImages event, _Emit emit) { + _logger.finer( + '_onUpdateLatteImages() called with batch.id=${event.batch?.id}', + ); + if (state.awaitingQuestions) { + final questionsUsedToBeMissing = + state.imagesBatch?[state.selectedImageIndex!]!.questions == null; + final questionsAreNowAvailable = + event.batch?[state.selectedImageIndex!]!.questions != null; + + if (questionsUsedToBeMissing && questionsAreNowAvailable) { + _logger.fine( + 'Questions are now available for Image ${state.selectedImageIndex}', + ); + _loadQuestions(event.batch); + } + } + emit(state.copyWith(imagesBatch: event.batch)); + } + + void _onNewImageLatteBatch(NewImageLatteBatch event, _Emit emit) { + _logger.finer( + '_onNewImageLatteBatch() called with id: ${event.latteImageBatchId}', + ); + if (state.currentStep == .tweakImage) { + _logger.fine( + 'Navigating to .chooseATweakedImage after answering questions', + ); + emit(state.copyWith(isSubmitting: false, selectedImageIndex: null)); + add(const GoToStep(.chooseATweakedImage)); + } else if (state.currentStep == .chooseATweakedImage) { + emit(state.copyWith(selectedImageIndex: null)); + } + } + + void _onUpdateQuestions(UpdateQuestions event, _Emit emit) { + _logger.info( + 'Loaded questions for index ${state.selectedImageIndex} :: ' + '${event.questions.length} questions', + ); + emit( + state.copyWith( + questions: event.questions, + awaitingQuestions: event.questions.isEmpty, + ), + ); + } + + void _onAnswerQuestion(AnswerQuestion event, _Emit emit) { + _logger.fine( + '_onAnswerQuestion() called for question ${event.question.id} ' + 'with answer: ${event.answer}', + ); + for (final (index, question) in state.questions.indexed) { + if (question.id == event.question.id) { + final questions = List.from( + state.questions, + ); + questions[index] = questions[index].copyWithAnswer(event.answer); + emit(state.copyWith(questions: questions)); + return; + } + } + _logger.warning('Question not found: ${event.question.id}'); + } + + Future _onGenerateRevisedImages( + GenerateRevisedImages event, + _Emit emit, + ) async { + _logger.finer('_onGenerateRevisedImages() called'); + emit( + state.copyWith( + isSubmitting: true, + imageGenerationStartTime: DateTime.now(), + ), + ); + + final answers = state.questions.fold( + {}, + (acc, question) { + acc[question.id] = question.answer; + return acc; + }, + ); + final imageIndex = switch (state.selectedImageIndex!) { + 0 => 'image0', + 1 => 'image1', + 2 => 'image2', + 3 => 'image3', + _ => throw Exception( + 'Unexpected image index: ${state.selectedImageIndex}', + ), + }; + _logger.info( + 'Generating revised images for ${state.imagesBatch!.id}.$imageIndex with ' + 'answers: $answers', + ); + _orderRepository + .generateRevisedImages( + imageBatchId: state.imagesBatch!.id, + imageIndex: imageIndex, + answers: answers, + ) + .then((_) {}) + .catchError((Object error) { + _logger.severe('Failed to generate revised images: $error'); + emit(state.copyWith(isSubmitting: false)); + if (state.currentStep != .tweakImage) { + // If the function fails, return to this step should we have + // navigated forward. + add(const GoToStep(.tweakImage)); + } + }) + .ignore(); + + emit(state.copyWith(imagesBatch: null)); + // Optimistically navigate forward. + add(const GoToStep(.chooseATweakedImage)); + } + + Future _onRejectImageBatch(RejectImageBatch event, _Emit emit) async { + _logger.finer('_onRejectImageBatch() called'); + assert( + state.imagesBatch?.parent != null, + 'Should not be reverting images without an ImageBatch with a parent', + ); + + emit(state.copyWith(isReverting: true)); + await _orderRepository.rejectImageBatch(state.imagesBatch!.id); + _watchImageBatch(state.imagesBatch!.parent!.id); + add(const GoToStep(.chooseAnImage)); + } + + void _onAcceptImage(AcceptImage event, _Emit emit) { + _logger.finer('_onAcceptImage() called'); + assert( + state.selectedImageIndex != null, + 'Should not be accepting images without a selected image', + ); + + // Check whether we're passing through again with an already-accepted + // image. + if (state.metadata!.imageUrl != null && + state.metadata!.imageUrl == + state.imagesBatch![state.selectedImageIndex!]?.imageUrl) { + return add(const GoToStep(.submitOrder)); + } + + emit(state.copyWith(isSubmitting: true)); + _orderRepository + .acceptImage( + imageBatchId: state.imagesBatch!.id, + imageIndex: switch (state.selectedImageIndex!) { + 0 => 'image0', + 1 => 'image1', + 2 => 'image2', + 3 => 'image3', + _ => throw Exception( + 'Unexpected image index: ${state.selectedImageIndex}', + ), + }, + ) + .then((_) { + _logger.fine('Image accepted and order submitted for moderation'); + }) + .catchError((Object error) { + _logger.severe('Failed to accept image and submit order: $error'); + emit(state.copyWith(isSubmitting: false)); + }) + .ignore(); + } + + Future _onSubmitOrder(SubmitOrder event, _Emit emit) async { + _logger.finer('_onSubmitOrder() called'); + assert( + state.metadata?.imageUrl != null, + 'Should not be submitting order without an image', + ); + emit(state.copyWith(isSubmitting: true)); + await _orderRepository + .submitOrder(state.metadata!.id!) + .then((_) { + _logger.fine('Order ${state.metadata!.id} submitted'); + }) + .catchError((Object error) { + _logger.severe('Failed to submit order: $error'); + emit(state.copyWith(isSubmitting: false)); + }); + } + + void _onNewPage(OnNewPage event, _Emit emit) { + _logger.finer('_onNewPage() called with .${event.newStep.name}'); + switch (event.newStep) { + case KioskWizardStep.intro: + if (state.shouldClearData) { + add(const _EjectData()); + } + case KioskWizardStep.name || + KioskWizardStep.milkAndSweetener || + KioskWizardStep.happyPlace || + KioskWizardStep.chooseAnImage || + KioskWizardStep.tweakImage || + KioskWizardStep.chooseATweakedImage || + KioskWizardStep.submitOrder || + KioskWizardStep.confirmation: + // TODO(craiglabenz): Uncomment this after testing milk resetting bug + // (https://github.com/GoogleCloudDemos/gcdemos-26-int-dd-latteart/issues/198) + // if (state.shouldClearData) { + // emit(state.copyWith(shouldClearData: false)); + // } + break; + } + } + + /// [emit] is nullable because when this is called during absolute + /// initialization, an emitter function is both unavailable and we do not need + /// to worry about clearing the `isSubmitting` field. + void _loadForStep(KioskWizardStep step, _Emit? emit) { + if (state.isSubmitting || state.isReverting) { + // The one thing we know is that, upon arriving at a new step, we are no + // longer submitting whatever data concluded the previous step. + emit?.call(state.copyWith(isSubmitting: false, isReverting: false)); + } + switch (step) { + case KioskWizardStep.intro: + break; + case KioskWizardStep.name: + break; + case KioskWizardStep.milkAndSweetener: + break; + case KioskWizardStep.happyPlace: + break; + case KioskWizardStep.chooseAnImage: + break; + case KioskWizardStep.tweakImage: + _loadQuestions(); + case KioskWizardStep.chooseATweakedImage: + break; + case KioskWizardStep.submitOrder: + break; + case KioskWizardStep.confirmation: + break; + } + } + + /// Sends questions from the selected image to the UI. + /// + /// Optionally pass a specific batch to use if the server has just sent an + /// updated version of the image batch which we know freshly contains these + /// very questions that we care about. Otherwise, read from the existing + /// state variable. + void _loadQuestions([LatteImageBatch? batch]) { + final batchToUse = batch ?? state.imagesBatch; + if (batchToUse != null && state.selectedImageIndex != null) { + final images = batchToUse.images.toList(); + final selectedImage = images[state.selectedImageIndex!]; + if (selectedImage?.questions != null) { + add(UpdateQuestions(selectedImage!.questions!)); + return; + } + } + add(const UpdateQuestions([])); + } + + void _onSelectImage(SelectImage event, _Emit emit) { + _logger.fine('_onSelectImage() called with index: ${event.index}'); + emit(state.copyWith(selectedImageIndex: event.index)); + } + + @override + Future close() { + _milkOptionsSubscription?.cancel().ignore(); + _sweetenerOptionsSubscription?.cancel().ignore(); + _orderSubscription?.cancel().ignore(); + _metadataSubscription?.cancel().ignore(); + _imageBatchSubscription?.cancel().ignore(); + return super.close(); + } +} + +/// Actions that can be taken on the KioskHome page. +@Freezed() +sealed class KioskHomeEvent with _$KioskHomeEvent { + /// A new page is being shown. Called once at the beginning of a new page's + /// animation into the screen. + const factory KioskHomeEvent.onNewPage(KioskWizardStep newStep) = OnNewPage; + + /// Sends the user back to the intro screen and sets a flag which will + /// cause any existing orders to be ejected from memory when the user arrives + /// there. + const factory KioskHomeEvent.startOver() = StartOver; + + /// An internal event thrown after [StartOver]. + const factory KioskHomeEvent.ejectData() = _EjectData; + + /// Possibly triggered on start-up if an existing, partially finished + /// [LatteOrder] is found locally. This allows a restarted browser to not lose + /// progress. + const factory KioskHomeEvent.loadPreExistingOrders( + List orders, + ) = LoadPreExistingOrders; + + /// Overwrites all state for a given order; used to resume progress after + /// [LoadPreExistingOrders] has succeeded. + const factory KioskHomeEvent.applyPreExistingOrder( + LatteOrder order, + LatteOrderMetadata metadata, + ) = ApplyPreExistingOrder; + + /// Refreshed milk options have arrived from the server. + const factory KioskHomeEvent.updateMilkOptions( + List? milkOptions, + ) = UpdateMilkOptions; + + /// Refreshed sweetener options have arrived from the server. + const factory KioskHomeEvent.updateSweetenerOptions( + List? sweetenerOptions, + ) = UpdateSweetenerOptions; + + /// Locally set the user's preferred milk. + const factory KioskHomeEvent.selectMilk(String milk) = SelectMilk; + + /// Locally set the user's preferred sweetener. + const factory KioskHomeEvent.selectSweetener(String sweetener) = + SelectSweetener; + + /// Submit the user's milk and sweetener preferences. + const factory KioskHomeEvent.submitMilkAndSweetener() = + SubmitMilkAndSweetener; + + /// Submit the user's happy place. + const factory KioskHomeEvent.submitHappyPlace(String happyPlace) = + SubmitHappyPlace; + + /// Gemini has rejected the user's happy place. + const factory KioskHomeEvent.happyPlaceRejected(String reason) = + HappyPlaceRejected; + + /// The user has been shown the happy place moderation reason, meaning we can + /// clear it from the state so it isn't shown on every subsequent change. + const factory KioskHomeEvent.happyPlaceModerationReasonShown() = + HappyPlaceModerationReasonShown; + + /// The [LatteOrder] has been updated by the server. + const factory KioskHomeEvent.serverOrderUpdate(LatteOrder? order) = + ServerOrderUpdate; + + /// The [LatteOrderMetadata] has been updated by the server. + const factory KioskHomeEvent.serverMetadataUpdate( + LatteOrderMetadata? metadata, + ) = ServerMetadataUpdate; + + /// Go back to the previous step in the wizard. + const factory KioskHomeEvent.goBack() = GoBackKioskWizard; + + /// Go to a specific step in the wizard. + const factory KioskHomeEvent.goToStep(KioskWizardStep step) = GoToStep; + + /// Submit the user's name. + const factory KioskHomeEvent.submitUserName(String name) = SubmitUserName; + + /// The user has selected an image from within the batch. Note that + /// this only amounts to tapping on the image. It does not mean the image + /// has been fully accepted for a submitted order. + const factory KioskHomeEvent.selectImage(int index) = SelectImage; + + /// The server has updated the [LatteImageBatch] that is being watched. Note + /// that this is different from the server changing *which* [LatteImageBatch] + /// is being watched. For that event, see [NewImageLatteBatch]. + const factory KioskHomeEvent.updateLatteImages(LatteImageBatch? batch) = + UpdateLatteImages; + + /// An update to the watched [LatteOrderMetadata] object has returned a new + /// [LatteImageBatch] id. This is different from changes to the currently + /// watched [LatteImageBatch] id. For that, see [UpdateLatteImages]. + const factory KioskHomeEvent.newImageLatteBatch(String latteImageBatchId) = + NewImageLatteBatch; + + /// Update the list of available questions from a selected image. This fires + /// when the user selects an image to tweak. + const factory KioskHomeEvent.updateQuestions(List questions) = + UpdateQuestions; + + /// Saves a user's answer to a question. + const factory KioskHomeEvent.answerQuestion( + Question question, + Object? answer, + ) = AnswerQuestion; + + /// Sends the user's answers to the questions to the backend to generate + /// new images. If this call to the server is successful, a + /// [NewImageLatteBatch] event is expected in short order. + const factory KioskHomeEvent.generateRevisedImages() = GenerateRevisedImages; + + /// The user has rejected the revised images. + const factory KioskHomeEvent.rejectImageBatch() = RejectImageBatch; + + /// The user has accepted their selected image. + const factory KioskHomeEvent.acceptImage() = AcceptImage; + + /// The user has submitted their finalized order. + const factory KioskHomeEvent.submitOrder() = SubmitOrder; +} + +/// {@template KioskHomeState} +/// Complete representation of the KioskHome page's state. +/// {@endtemplate +@Freezed() +abstract class KioskHomeState with _$KioskHomeState { + /// {@macro KioskHomeState} + const factory KioskHomeState({ + required KioskWizardStep currentStep, + LatteOrder? order, + LatteOrderMetadata? metadata, + ModerationEvent? happyPlaceModerationEvent, + @Default(false) bool isSubmitting, + @Default(false) bool isReverting, + + /// When the user submits a happy place, or a tweak, + /// the clock starts ticking on image generation. + /// We want to track this time. If for nothing else, it helps us show + /// a progress bar that doesn't depend on widget states being mounted + /// throughout the image generation without gaps. + DateTime? imageGenerationStartTime, + + /// When an image's questions are requested before they exist, we store the + /// need here so that we add them to the state once they arrive. + @Default(false) bool awaitingQuestions, + + /// The current batch of 4 images to choose from. This is deduced by the + /// [LatteOrderMetadata]'s `latteBatchId` field. + LatteImageBatch? imagesBatch, + + /// Copy of the active [LatteImage]'s questions, only set once the user + /// chooses an image and clicks the "tweak image" button. This copy is used + /// instead of the questions in the [LatteImage] to isolate the user's + /// answers from changes to the [LatteImageBatch]. The critical detail is + /// that answers are not persisted to Firebase, which means anything that + /// overwrites the [LatteImageBatch] would clobber the user's answers. + @Default([]) List questions, + + /// Values loaded straight from Firebase. + @LatteOptionConverter() List? milkOptions, + + /// Values loaded straight from Firebase. + @LatteOptionConverter() List? sweetenerOptions, + + /// True if the user is starting over and we want to eject data upon + /// returning to the .intro screen. + @Default(false) bool shouldClearData, + + int? selectedImageIndex, + }) = _KioskHomeState; + const KioskHomeState._(); + + /// Starter state fed to the [KioskHomeBloc]. + factory KioskHomeState.initial() => KioskHomeState( + currentStep: KioskWizardStep.values.first, + ); +} + +/// The steps in the kiosk wizard. +enum KioskWizardStep { + /// Establishing expectations / describing the process. + intro, + + /// Collecting the user's name. + name, + + /// Collecting the user's milk / sweetener preferences. + milkAndSweetener, + + /// Collecting the user's happy place. + happyPlace, + + /// The user's first time choosing 1 of 4 images + chooseAnImage, + + /// Answer Gemini's questions to configure the image + tweakImage, + + /// Similar to [chooseAnImage], but with extra awareness that this is a forked + /// image batch which the user could outright reject + chooseATweakedImage, + + /// Order review screen where the user submits their order. + submitOrder, + + /// Post-submission confirmation screen to conclude the user's journey. + confirmation, + ; + + /// The current step's place in line. + int get stepIndex => values.indexOf(this); + + /// Returns the given step's next step. + KioskWizardStep? get nextStep { + final index = stepIndex; + if (index >= totalSteps) { + return null; + } + return fromIndex(index + 1); + } + + /// Returns the given step's previous step. + KioskWizardStep? get previousStep { + final index = stepIndex; + if (index <= 0) { + return null; + } + return fromIndex(index - 1); + } + + /// The step at the given index. + static KioskWizardStep fromIndex(int index) => values[index]; + + /// True if this is the first step. + bool get isFirstStep => this == values.first; + + /// True if this is the last step. + bool get isLastStep => this == values.last; + + /// The total number of steps in the wizard. + int get totalSteps => values.length; + + /// The index to use for the progress indicator. + int get progressIndicatorIndex => switch (this) { + KioskWizardStep.intro => 0, + KioskWizardStep.name => 0, + KioskWizardStep.milkAndSweetener => 1, + KioskWizardStep.happyPlace => 2, + KioskWizardStep.chooseAnImage => 2, + KioskWizardStep.tweakImage => 2, + KioskWizardStep.chooseATweakedImage => 2, + KioskWizardStep.submitOrder => 3, + KioskWizardStep.confirmation => 3, + }; +} + +/// Bundles a server moderation reason with a Uuidv4 which is used to help +/// the UI display only 1 toast per moderation event. +@immutable +class ModerationEvent { + /// Instantiates a [ModerationEvent]. + ModerationEvent(this.reason) : id = const Uuid().v4(); + + /// The moderation reason. + final String reason; + + /// Unique Id. + final String id; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ModerationEvent && + runtimeType == other.runtimeType && + reason == other.reason && + id == other.id; + + @override + int get hashCode => reason.hashCode ^ id.hashCode; +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home_bloc.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home_bloc.freezed.dart new file mode 100644 index 0000000..bee12fb --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home_bloc.freezed.dart @@ -0,0 +1,2334 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'kiosk_home_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$KioskHomeEvent { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is KioskHomeEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent()'; +} + + +} + +/// @nodoc +class $KioskHomeEventCopyWith<$Res> { +$KioskHomeEventCopyWith(KioskHomeEvent _, $Res Function(KioskHomeEvent) __); +} + + +/// Adds pattern-matching-related methods to [KioskHomeEvent]. +extension KioskHomeEventPatterns on KioskHomeEvent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( OnNewPage value)? onNewPage,TResult Function( StartOver value)? startOver,TResult Function( _EjectData value)? ejectData,TResult Function( LoadPreExistingOrders value)? loadPreExistingOrders,TResult Function( ApplyPreExistingOrder value)? applyPreExistingOrder,TResult Function( UpdateMilkOptions value)? updateMilkOptions,TResult Function( UpdateSweetenerOptions value)? updateSweetenerOptions,TResult Function( SelectMilk value)? selectMilk,TResult Function( SelectSweetener value)? selectSweetener,TResult Function( SubmitMilkAndSweetener value)? submitMilkAndSweetener,TResult Function( SubmitHappyPlace value)? submitHappyPlace,TResult Function( HappyPlaceRejected value)? happyPlaceRejected,TResult Function( HappyPlaceModerationReasonShown value)? happyPlaceModerationReasonShown,TResult Function( ServerOrderUpdate value)? serverOrderUpdate,TResult Function( ServerMetadataUpdate value)? serverMetadataUpdate,TResult Function( GoBackKioskWizard value)? goBack,TResult Function( GoToStep value)? goToStep,TResult Function( SubmitUserName value)? submitUserName,TResult Function( SelectImage value)? selectImage,TResult Function( UpdateLatteImages value)? updateLatteImages,TResult Function( NewImageLatteBatch value)? newImageLatteBatch,TResult Function( UpdateQuestions value)? updateQuestions,TResult Function( AnswerQuestion value)? answerQuestion,TResult Function( GenerateRevisedImages value)? generateRevisedImages,TResult Function( RejectImageBatch value)? rejectImageBatch,TResult Function( AcceptImage value)? acceptImage,TResult Function( SubmitOrder value)? submitOrder,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case OnNewPage() when onNewPage != null: +return onNewPage(_that);case StartOver() when startOver != null: +return startOver(_that);case _EjectData() when ejectData != null: +return ejectData(_that);case LoadPreExistingOrders() when loadPreExistingOrders != null: +return loadPreExistingOrders(_that);case ApplyPreExistingOrder() when applyPreExistingOrder != null: +return applyPreExistingOrder(_that);case UpdateMilkOptions() when updateMilkOptions != null: +return updateMilkOptions(_that);case UpdateSweetenerOptions() when updateSweetenerOptions != null: +return updateSweetenerOptions(_that);case SelectMilk() when selectMilk != null: +return selectMilk(_that);case SelectSweetener() when selectSweetener != null: +return selectSweetener(_that);case SubmitMilkAndSweetener() when submitMilkAndSweetener != null: +return submitMilkAndSweetener(_that);case SubmitHappyPlace() when submitHappyPlace != null: +return submitHappyPlace(_that);case HappyPlaceRejected() when happyPlaceRejected != null: +return happyPlaceRejected(_that);case HappyPlaceModerationReasonShown() when happyPlaceModerationReasonShown != null: +return happyPlaceModerationReasonShown(_that);case ServerOrderUpdate() when serverOrderUpdate != null: +return serverOrderUpdate(_that);case ServerMetadataUpdate() when serverMetadataUpdate != null: +return serverMetadataUpdate(_that);case GoBackKioskWizard() when goBack != null: +return goBack(_that);case GoToStep() when goToStep != null: +return goToStep(_that);case SubmitUserName() when submitUserName != null: +return submitUserName(_that);case SelectImage() when selectImage != null: +return selectImage(_that);case UpdateLatteImages() when updateLatteImages != null: +return updateLatteImages(_that);case NewImageLatteBatch() when newImageLatteBatch != null: +return newImageLatteBatch(_that);case UpdateQuestions() when updateQuestions != null: +return updateQuestions(_that);case AnswerQuestion() when answerQuestion != null: +return answerQuestion(_that);case GenerateRevisedImages() when generateRevisedImages != null: +return generateRevisedImages(_that);case RejectImageBatch() when rejectImageBatch != null: +return rejectImageBatch(_that);case AcceptImage() when acceptImage != null: +return acceptImage(_that);case SubmitOrder() when submitOrder != null: +return submitOrder(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( OnNewPage value) onNewPage,required TResult Function( StartOver value) startOver,required TResult Function( _EjectData value) ejectData,required TResult Function( LoadPreExistingOrders value) loadPreExistingOrders,required TResult Function( ApplyPreExistingOrder value) applyPreExistingOrder,required TResult Function( UpdateMilkOptions value) updateMilkOptions,required TResult Function( UpdateSweetenerOptions value) updateSweetenerOptions,required TResult Function( SelectMilk value) selectMilk,required TResult Function( SelectSweetener value) selectSweetener,required TResult Function( SubmitMilkAndSweetener value) submitMilkAndSweetener,required TResult Function( SubmitHappyPlace value) submitHappyPlace,required TResult Function( HappyPlaceRejected value) happyPlaceRejected,required TResult Function( HappyPlaceModerationReasonShown value) happyPlaceModerationReasonShown,required TResult Function( ServerOrderUpdate value) serverOrderUpdate,required TResult Function( ServerMetadataUpdate value) serverMetadataUpdate,required TResult Function( GoBackKioskWizard value) goBack,required TResult Function( GoToStep value) goToStep,required TResult Function( SubmitUserName value) submitUserName,required TResult Function( SelectImage value) selectImage,required TResult Function( UpdateLatteImages value) updateLatteImages,required TResult Function( NewImageLatteBatch value) newImageLatteBatch,required TResult Function( UpdateQuestions value) updateQuestions,required TResult Function( AnswerQuestion value) answerQuestion,required TResult Function( GenerateRevisedImages value) generateRevisedImages,required TResult Function( RejectImageBatch value) rejectImageBatch,required TResult Function( AcceptImage value) acceptImage,required TResult Function( SubmitOrder value) submitOrder,}){ +final _that = this; +switch (_that) { +case OnNewPage(): +return onNewPage(_that);case StartOver(): +return startOver(_that);case _EjectData(): +return ejectData(_that);case LoadPreExistingOrders(): +return loadPreExistingOrders(_that);case ApplyPreExistingOrder(): +return applyPreExistingOrder(_that);case UpdateMilkOptions(): +return updateMilkOptions(_that);case UpdateSweetenerOptions(): +return updateSweetenerOptions(_that);case SelectMilk(): +return selectMilk(_that);case SelectSweetener(): +return selectSweetener(_that);case SubmitMilkAndSweetener(): +return submitMilkAndSweetener(_that);case SubmitHappyPlace(): +return submitHappyPlace(_that);case HappyPlaceRejected(): +return happyPlaceRejected(_that);case HappyPlaceModerationReasonShown(): +return happyPlaceModerationReasonShown(_that);case ServerOrderUpdate(): +return serverOrderUpdate(_that);case ServerMetadataUpdate(): +return serverMetadataUpdate(_that);case GoBackKioskWizard(): +return goBack(_that);case GoToStep(): +return goToStep(_that);case SubmitUserName(): +return submitUserName(_that);case SelectImage(): +return selectImage(_that);case UpdateLatteImages(): +return updateLatteImages(_that);case NewImageLatteBatch(): +return newImageLatteBatch(_that);case UpdateQuestions(): +return updateQuestions(_that);case AnswerQuestion(): +return answerQuestion(_that);case GenerateRevisedImages(): +return generateRevisedImages(_that);case RejectImageBatch(): +return rejectImageBatch(_that);case AcceptImage(): +return acceptImage(_that);case SubmitOrder(): +return submitOrder(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( OnNewPage value)? onNewPage,TResult? Function( StartOver value)? startOver,TResult? Function( _EjectData value)? ejectData,TResult? Function( LoadPreExistingOrders value)? loadPreExistingOrders,TResult? Function( ApplyPreExistingOrder value)? applyPreExistingOrder,TResult? Function( UpdateMilkOptions value)? updateMilkOptions,TResult? Function( UpdateSweetenerOptions value)? updateSweetenerOptions,TResult? Function( SelectMilk value)? selectMilk,TResult? Function( SelectSweetener value)? selectSweetener,TResult? Function( SubmitMilkAndSweetener value)? submitMilkAndSweetener,TResult? Function( SubmitHappyPlace value)? submitHappyPlace,TResult? Function( HappyPlaceRejected value)? happyPlaceRejected,TResult? Function( HappyPlaceModerationReasonShown value)? happyPlaceModerationReasonShown,TResult? Function( ServerOrderUpdate value)? serverOrderUpdate,TResult? Function( ServerMetadataUpdate value)? serverMetadataUpdate,TResult? Function( GoBackKioskWizard value)? goBack,TResult? Function( GoToStep value)? goToStep,TResult? Function( SubmitUserName value)? submitUserName,TResult? Function( SelectImage value)? selectImage,TResult? Function( UpdateLatteImages value)? updateLatteImages,TResult? Function( NewImageLatteBatch value)? newImageLatteBatch,TResult? Function( UpdateQuestions value)? updateQuestions,TResult? Function( AnswerQuestion value)? answerQuestion,TResult? Function( GenerateRevisedImages value)? generateRevisedImages,TResult? Function( RejectImageBatch value)? rejectImageBatch,TResult? Function( AcceptImage value)? acceptImage,TResult? Function( SubmitOrder value)? submitOrder,}){ +final _that = this; +switch (_that) { +case OnNewPage() when onNewPage != null: +return onNewPage(_that);case StartOver() when startOver != null: +return startOver(_that);case _EjectData() when ejectData != null: +return ejectData(_that);case LoadPreExistingOrders() when loadPreExistingOrders != null: +return loadPreExistingOrders(_that);case ApplyPreExistingOrder() when applyPreExistingOrder != null: +return applyPreExistingOrder(_that);case UpdateMilkOptions() when updateMilkOptions != null: +return updateMilkOptions(_that);case UpdateSweetenerOptions() when updateSweetenerOptions != null: +return updateSweetenerOptions(_that);case SelectMilk() when selectMilk != null: +return selectMilk(_that);case SelectSweetener() when selectSweetener != null: +return selectSweetener(_that);case SubmitMilkAndSweetener() when submitMilkAndSweetener != null: +return submitMilkAndSweetener(_that);case SubmitHappyPlace() when submitHappyPlace != null: +return submitHappyPlace(_that);case HappyPlaceRejected() when happyPlaceRejected != null: +return happyPlaceRejected(_that);case HappyPlaceModerationReasonShown() when happyPlaceModerationReasonShown != null: +return happyPlaceModerationReasonShown(_that);case ServerOrderUpdate() when serverOrderUpdate != null: +return serverOrderUpdate(_that);case ServerMetadataUpdate() when serverMetadataUpdate != null: +return serverMetadataUpdate(_that);case GoBackKioskWizard() when goBack != null: +return goBack(_that);case GoToStep() when goToStep != null: +return goToStep(_that);case SubmitUserName() when submitUserName != null: +return submitUserName(_that);case SelectImage() when selectImage != null: +return selectImage(_that);case UpdateLatteImages() when updateLatteImages != null: +return updateLatteImages(_that);case NewImageLatteBatch() when newImageLatteBatch != null: +return newImageLatteBatch(_that);case UpdateQuestions() when updateQuestions != null: +return updateQuestions(_that);case AnswerQuestion() when answerQuestion != null: +return answerQuestion(_that);case GenerateRevisedImages() when generateRevisedImages != null: +return generateRevisedImages(_that);case RejectImageBatch() when rejectImageBatch != null: +return rejectImageBatch(_that);case AcceptImage() when acceptImage != null: +return acceptImage(_that);case SubmitOrder() when submitOrder != null: +return submitOrder(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( KioskWizardStep newStep)? onNewPage,TResult Function()? startOver,TResult Function()? ejectData,TResult Function( List orders)? loadPreExistingOrders,TResult Function( LatteOrder order, LatteOrderMetadata metadata)? applyPreExistingOrder,TResult Function( List? milkOptions)? updateMilkOptions,TResult Function( List? sweetenerOptions)? updateSweetenerOptions,TResult Function( String milk)? selectMilk,TResult Function( String sweetener)? selectSweetener,TResult Function()? submitMilkAndSweetener,TResult Function( String happyPlace)? submitHappyPlace,TResult Function( String reason)? happyPlaceRejected,TResult Function()? happyPlaceModerationReasonShown,TResult Function( LatteOrder? order)? serverOrderUpdate,TResult Function( LatteOrderMetadata? metadata)? serverMetadataUpdate,TResult Function()? goBack,TResult Function( KioskWizardStep step)? goToStep,TResult Function( String name)? submitUserName,TResult Function( int index)? selectImage,TResult Function( LatteImageBatch? batch)? updateLatteImages,TResult Function( String latteImageBatchId)? newImageLatteBatch,TResult Function( List questions)? updateQuestions,TResult Function( Question question, Object? answer)? answerQuestion,TResult Function()? generateRevisedImages,TResult Function()? rejectImageBatch,TResult Function()? acceptImage,TResult Function()? submitOrder,required TResult orElse(),}) {final _that = this; +switch (_that) { +case OnNewPage() when onNewPage != null: +return onNewPage(_that.newStep);case StartOver() when startOver != null: +return startOver();case _EjectData() when ejectData != null: +return ejectData();case LoadPreExistingOrders() when loadPreExistingOrders != null: +return loadPreExistingOrders(_that.orders);case ApplyPreExistingOrder() when applyPreExistingOrder != null: +return applyPreExistingOrder(_that.order,_that.metadata);case UpdateMilkOptions() when updateMilkOptions != null: +return updateMilkOptions(_that.milkOptions);case UpdateSweetenerOptions() when updateSweetenerOptions != null: +return updateSweetenerOptions(_that.sweetenerOptions);case SelectMilk() when selectMilk != null: +return selectMilk(_that.milk);case SelectSweetener() when selectSweetener != null: +return selectSweetener(_that.sweetener);case SubmitMilkAndSweetener() when submitMilkAndSweetener != null: +return submitMilkAndSweetener();case SubmitHappyPlace() when submitHappyPlace != null: +return submitHappyPlace(_that.happyPlace);case HappyPlaceRejected() when happyPlaceRejected != null: +return happyPlaceRejected(_that.reason);case HappyPlaceModerationReasonShown() when happyPlaceModerationReasonShown != null: +return happyPlaceModerationReasonShown();case ServerOrderUpdate() when serverOrderUpdate != null: +return serverOrderUpdate(_that.order);case ServerMetadataUpdate() when serverMetadataUpdate != null: +return serverMetadataUpdate(_that.metadata);case GoBackKioskWizard() when goBack != null: +return goBack();case GoToStep() when goToStep != null: +return goToStep(_that.step);case SubmitUserName() when submitUserName != null: +return submitUserName(_that.name);case SelectImage() when selectImage != null: +return selectImage(_that.index);case UpdateLatteImages() when updateLatteImages != null: +return updateLatteImages(_that.batch);case NewImageLatteBatch() when newImageLatteBatch != null: +return newImageLatteBatch(_that.latteImageBatchId);case UpdateQuestions() when updateQuestions != null: +return updateQuestions(_that.questions);case AnswerQuestion() when answerQuestion != null: +return answerQuestion(_that.question,_that.answer);case GenerateRevisedImages() when generateRevisedImages != null: +return generateRevisedImages();case RejectImageBatch() when rejectImageBatch != null: +return rejectImageBatch();case AcceptImage() when acceptImage != null: +return acceptImage();case SubmitOrder() when submitOrder != null: +return submitOrder();case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( KioskWizardStep newStep) onNewPage,required TResult Function() startOver,required TResult Function() ejectData,required TResult Function( List orders) loadPreExistingOrders,required TResult Function( LatteOrder order, LatteOrderMetadata metadata) applyPreExistingOrder,required TResult Function( List? milkOptions) updateMilkOptions,required TResult Function( List? sweetenerOptions) updateSweetenerOptions,required TResult Function( String milk) selectMilk,required TResult Function( String sweetener) selectSweetener,required TResult Function() submitMilkAndSweetener,required TResult Function( String happyPlace) submitHappyPlace,required TResult Function( String reason) happyPlaceRejected,required TResult Function() happyPlaceModerationReasonShown,required TResult Function( LatteOrder? order) serverOrderUpdate,required TResult Function( LatteOrderMetadata? metadata) serverMetadataUpdate,required TResult Function() goBack,required TResult Function( KioskWizardStep step) goToStep,required TResult Function( String name) submitUserName,required TResult Function( int index) selectImage,required TResult Function( LatteImageBatch? batch) updateLatteImages,required TResult Function( String latteImageBatchId) newImageLatteBatch,required TResult Function( List questions) updateQuestions,required TResult Function( Question question, Object? answer) answerQuestion,required TResult Function() generateRevisedImages,required TResult Function() rejectImageBatch,required TResult Function() acceptImage,required TResult Function() submitOrder,}) {final _that = this; +switch (_that) { +case OnNewPage(): +return onNewPage(_that.newStep);case StartOver(): +return startOver();case _EjectData(): +return ejectData();case LoadPreExistingOrders(): +return loadPreExistingOrders(_that.orders);case ApplyPreExistingOrder(): +return applyPreExistingOrder(_that.order,_that.metadata);case UpdateMilkOptions(): +return updateMilkOptions(_that.milkOptions);case UpdateSweetenerOptions(): +return updateSweetenerOptions(_that.sweetenerOptions);case SelectMilk(): +return selectMilk(_that.milk);case SelectSweetener(): +return selectSweetener(_that.sweetener);case SubmitMilkAndSweetener(): +return submitMilkAndSweetener();case SubmitHappyPlace(): +return submitHappyPlace(_that.happyPlace);case HappyPlaceRejected(): +return happyPlaceRejected(_that.reason);case HappyPlaceModerationReasonShown(): +return happyPlaceModerationReasonShown();case ServerOrderUpdate(): +return serverOrderUpdate(_that.order);case ServerMetadataUpdate(): +return serverMetadataUpdate(_that.metadata);case GoBackKioskWizard(): +return goBack();case GoToStep(): +return goToStep(_that.step);case SubmitUserName(): +return submitUserName(_that.name);case SelectImage(): +return selectImage(_that.index);case UpdateLatteImages(): +return updateLatteImages(_that.batch);case NewImageLatteBatch(): +return newImageLatteBatch(_that.latteImageBatchId);case UpdateQuestions(): +return updateQuestions(_that.questions);case AnswerQuestion(): +return answerQuestion(_that.question,_that.answer);case GenerateRevisedImages(): +return generateRevisedImages();case RejectImageBatch(): +return rejectImageBatch();case AcceptImage(): +return acceptImage();case SubmitOrder(): +return submitOrder();} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( KioskWizardStep newStep)? onNewPage,TResult? Function()? startOver,TResult? Function()? ejectData,TResult? Function( List orders)? loadPreExistingOrders,TResult? Function( LatteOrder order, LatteOrderMetadata metadata)? applyPreExistingOrder,TResult? Function( List? milkOptions)? updateMilkOptions,TResult? Function( List? sweetenerOptions)? updateSweetenerOptions,TResult? Function( String milk)? selectMilk,TResult? Function( String sweetener)? selectSweetener,TResult? Function()? submitMilkAndSweetener,TResult? Function( String happyPlace)? submitHappyPlace,TResult? Function( String reason)? happyPlaceRejected,TResult? Function()? happyPlaceModerationReasonShown,TResult? Function( LatteOrder? order)? serverOrderUpdate,TResult? Function( LatteOrderMetadata? metadata)? serverMetadataUpdate,TResult? Function()? goBack,TResult? Function( KioskWizardStep step)? goToStep,TResult? Function( String name)? submitUserName,TResult? Function( int index)? selectImage,TResult? Function( LatteImageBatch? batch)? updateLatteImages,TResult? Function( String latteImageBatchId)? newImageLatteBatch,TResult? Function( List questions)? updateQuestions,TResult? Function( Question question, Object? answer)? answerQuestion,TResult? Function()? generateRevisedImages,TResult? Function()? rejectImageBatch,TResult? Function()? acceptImage,TResult? Function()? submitOrder,}) {final _that = this; +switch (_that) { +case OnNewPage() when onNewPage != null: +return onNewPage(_that.newStep);case StartOver() when startOver != null: +return startOver();case _EjectData() when ejectData != null: +return ejectData();case LoadPreExistingOrders() when loadPreExistingOrders != null: +return loadPreExistingOrders(_that.orders);case ApplyPreExistingOrder() when applyPreExistingOrder != null: +return applyPreExistingOrder(_that.order,_that.metadata);case UpdateMilkOptions() when updateMilkOptions != null: +return updateMilkOptions(_that.milkOptions);case UpdateSweetenerOptions() when updateSweetenerOptions != null: +return updateSweetenerOptions(_that.sweetenerOptions);case SelectMilk() when selectMilk != null: +return selectMilk(_that.milk);case SelectSweetener() when selectSweetener != null: +return selectSweetener(_that.sweetener);case SubmitMilkAndSweetener() when submitMilkAndSweetener != null: +return submitMilkAndSweetener();case SubmitHappyPlace() when submitHappyPlace != null: +return submitHappyPlace(_that.happyPlace);case HappyPlaceRejected() when happyPlaceRejected != null: +return happyPlaceRejected(_that.reason);case HappyPlaceModerationReasonShown() when happyPlaceModerationReasonShown != null: +return happyPlaceModerationReasonShown();case ServerOrderUpdate() when serverOrderUpdate != null: +return serverOrderUpdate(_that.order);case ServerMetadataUpdate() when serverMetadataUpdate != null: +return serverMetadataUpdate(_that.metadata);case GoBackKioskWizard() when goBack != null: +return goBack();case GoToStep() when goToStep != null: +return goToStep(_that.step);case SubmitUserName() when submitUserName != null: +return submitUserName(_that.name);case SelectImage() when selectImage != null: +return selectImage(_that.index);case UpdateLatteImages() when updateLatteImages != null: +return updateLatteImages(_that.batch);case NewImageLatteBatch() when newImageLatteBatch != null: +return newImageLatteBatch(_that.latteImageBatchId);case UpdateQuestions() when updateQuestions != null: +return updateQuestions(_that.questions);case AnswerQuestion() when answerQuestion != null: +return answerQuestion(_that.question,_that.answer);case GenerateRevisedImages() when generateRevisedImages != null: +return generateRevisedImages();case RejectImageBatch() when rejectImageBatch != null: +return rejectImageBatch();case AcceptImage() when acceptImage != null: +return acceptImage();case SubmitOrder() when submitOrder != null: +return submitOrder();case _: + return null; + +} +} + +} + +/// @nodoc + + +class OnNewPage implements KioskHomeEvent { + const OnNewPage(this.newStep); + + + final KioskWizardStep newStep; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$OnNewPageCopyWith get copyWith => _$OnNewPageCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is OnNewPage&&(identical(other.newStep, newStep) || other.newStep == newStep)); +} + + +@override +int get hashCode => Object.hash(runtimeType,newStep); + +@override +String toString() { + return 'KioskHomeEvent.onNewPage(newStep: $newStep)'; +} + + +} + +/// @nodoc +abstract mixin class $OnNewPageCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $OnNewPageCopyWith(OnNewPage value, $Res Function(OnNewPage) _then) = _$OnNewPageCopyWithImpl; +@useResult +$Res call({ + KioskWizardStep newStep +}); + + + + +} +/// @nodoc +class _$OnNewPageCopyWithImpl<$Res> + implements $OnNewPageCopyWith<$Res> { + _$OnNewPageCopyWithImpl(this._self, this._then); + + final OnNewPage _self; + final $Res Function(OnNewPage) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? newStep = null,}) { + return _then(OnNewPage( +null == newStep ? _self.newStep : newStep // ignore: cast_nullable_to_non_nullable +as KioskWizardStep, + )); +} + + +} + +/// @nodoc + + +class StartOver implements KioskHomeEvent { + const StartOver(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is StartOver); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent.startOver()'; +} + + +} + + + + +/// @nodoc + + +class _EjectData implements KioskHomeEvent { + const _EjectData(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _EjectData); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent.ejectData()'; +} + + +} + + + + +/// @nodoc + + +class LoadPreExistingOrders implements KioskHomeEvent { + const LoadPreExistingOrders(final List orders): _orders = orders; + + + final List _orders; + List get orders { + if (_orders is EqualUnmodifiableListView) return _orders; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_orders); +} + + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LoadPreExistingOrdersCopyWith get copyWith => _$LoadPreExistingOrdersCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadPreExistingOrders&&const DeepCollectionEquality().equals(other._orders, _orders)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_orders)); + +@override +String toString() { + return 'KioskHomeEvent.loadPreExistingOrders(orders: $orders)'; +} + + +} + +/// @nodoc +abstract mixin class $LoadPreExistingOrdersCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $LoadPreExistingOrdersCopyWith(LoadPreExistingOrders value, $Res Function(LoadPreExistingOrders) _then) = _$LoadPreExistingOrdersCopyWithImpl; +@useResult +$Res call({ + List orders +}); + + + + +} +/// @nodoc +class _$LoadPreExistingOrdersCopyWithImpl<$Res> + implements $LoadPreExistingOrdersCopyWith<$Res> { + _$LoadPreExistingOrdersCopyWithImpl(this._self, this._then); + + final LoadPreExistingOrders _self; + final $Res Function(LoadPreExistingOrders) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? orders = null,}) { + return _then(LoadPreExistingOrders( +null == orders ? _self._orders : orders // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class ApplyPreExistingOrder implements KioskHomeEvent { + const ApplyPreExistingOrder(this.order, this.metadata); + + + final LatteOrder order; + final LatteOrderMetadata metadata; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ApplyPreExistingOrderCopyWith get copyWith => _$ApplyPreExistingOrderCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ApplyPreExistingOrder&&(identical(other.order, order) || other.order == order)&&(identical(other.metadata, metadata) || other.metadata == metadata)); +} + + +@override +int get hashCode => Object.hash(runtimeType,order,metadata); + +@override +String toString() { + return 'KioskHomeEvent.applyPreExistingOrder(order: $order, metadata: $metadata)'; +} + + +} + +/// @nodoc +abstract mixin class $ApplyPreExistingOrderCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $ApplyPreExistingOrderCopyWith(ApplyPreExistingOrder value, $Res Function(ApplyPreExistingOrder) _then) = _$ApplyPreExistingOrderCopyWithImpl; +@useResult +$Res call({ + LatteOrder order, LatteOrderMetadata metadata +}); + + +$LatteOrderCopyWith<$Res> get order;$LatteOrderMetadataCopyWith<$Res> get metadata; + +} +/// @nodoc +class _$ApplyPreExistingOrderCopyWithImpl<$Res> + implements $ApplyPreExistingOrderCopyWith<$Res> { + _$ApplyPreExistingOrderCopyWithImpl(this._self, this._then); + + final ApplyPreExistingOrder _self; + final $Res Function(ApplyPreExistingOrder) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? order = null,Object? metadata = null,}) { + return _then(ApplyPreExistingOrder( +null == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as LatteOrder,null == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable +as LatteOrderMetadata, + )); +} + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderCopyWith<$Res> get order { + + return $LatteOrderCopyWith<$Res>(_self.order, (value) { + return _then(_self.copyWith(order: value)); + }); +}/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderMetadataCopyWith<$Res> get metadata { + + return $LatteOrderMetadataCopyWith<$Res>(_self.metadata, (value) { + return _then(_self.copyWith(metadata: value)); + }); +} +} + +/// @nodoc + + +class UpdateMilkOptions implements KioskHomeEvent { + const UpdateMilkOptions(final List? milkOptions): _milkOptions = milkOptions; + + + final List? _milkOptions; + List? get milkOptions { + final value = _milkOptions; + if (value == null) return null; + if (_milkOptions is EqualUnmodifiableListView) return _milkOptions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$UpdateMilkOptionsCopyWith get copyWith => _$UpdateMilkOptionsCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdateMilkOptions&&const DeepCollectionEquality().equals(other._milkOptions, _milkOptions)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_milkOptions)); + +@override +String toString() { + return 'KioskHomeEvent.updateMilkOptions(milkOptions: $milkOptions)'; +} + + +} + +/// @nodoc +abstract mixin class $UpdateMilkOptionsCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $UpdateMilkOptionsCopyWith(UpdateMilkOptions value, $Res Function(UpdateMilkOptions) _then) = _$UpdateMilkOptionsCopyWithImpl; +@useResult +$Res call({ + List? milkOptions +}); + + + + +} +/// @nodoc +class _$UpdateMilkOptionsCopyWithImpl<$Res> + implements $UpdateMilkOptionsCopyWith<$Res> { + _$UpdateMilkOptionsCopyWithImpl(this._self, this._then); + + final UpdateMilkOptions _self; + final $Res Function(UpdateMilkOptions) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? milkOptions = freezed,}) { + return _then(UpdateMilkOptions( +freezed == milkOptions ? _self._milkOptions : milkOptions // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + + +} + +/// @nodoc + + +class UpdateSweetenerOptions implements KioskHomeEvent { + const UpdateSweetenerOptions(final List? sweetenerOptions): _sweetenerOptions = sweetenerOptions; + + + final List? _sweetenerOptions; + List? get sweetenerOptions { + final value = _sweetenerOptions; + if (value == null) return null; + if (_sweetenerOptions is EqualUnmodifiableListView) return _sweetenerOptions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$UpdateSweetenerOptionsCopyWith get copyWith => _$UpdateSweetenerOptionsCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdateSweetenerOptions&&const DeepCollectionEquality().equals(other._sweetenerOptions, _sweetenerOptions)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_sweetenerOptions)); + +@override +String toString() { + return 'KioskHomeEvent.updateSweetenerOptions(sweetenerOptions: $sweetenerOptions)'; +} + + +} + +/// @nodoc +abstract mixin class $UpdateSweetenerOptionsCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $UpdateSweetenerOptionsCopyWith(UpdateSweetenerOptions value, $Res Function(UpdateSweetenerOptions) _then) = _$UpdateSweetenerOptionsCopyWithImpl; +@useResult +$Res call({ + List? sweetenerOptions +}); + + + + +} +/// @nodoc +class _$UpdateSweetenerOptionsCopyWithImpl<$Res> + implements $UpdateSweetenerOptionsCopyWith<$Res> { + _$UpdateSweetenerOptionsCopyWithImpl(this._self, this._then); + + final UpdateSweetenerOptions _self; + final $Res Function(UpdateSweetenerOptions) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? sweetenerOptions = freezed,}) { + return _then(UpdateSweetenerOptions( +freezed == sweetenerOptions ? _self._sweetenerOptions : sweetenerOptions // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + + +} + +/// @nodoc + + +class SelectMilk implements KioskHomeEvent { + const SelectMilk(this.milk); + + + final String milk; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SelectMilkCopyWith get copyWith => _$SelectMilkCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SelectMilk&&(identical(other.milk, milk) || other.milk == milk)); +} + + +@override +int get hashCode => Object.hash(runtimeType,milk); + +@override +String toString() { + return 'KioskHomeEvent.selectMilk(milk: $milk)'; +} + + +} + +/// @nodoc +abstract mixin class $SelectMilkCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $SelectMilkCopyWith(SelectMilk value, $Res Function(SelectMilk) _then) = _$SelectMilkCopyWithImpl; +@useResult +$Res call({ + String milk +}); + + + + +} +/// @nodoc +class _$SelectMilkCopyWithImpl<$Res> + implements $SelectMilkCopyWith<$Res> { + _$SelectMilkCopyWithImpl(this._self, this._then); + + final SelectMilk _self; + final $Res Function(SelectMilk) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? milk = null,}) { + return _then(SelectMilk( +null == milk ? _self.milk : milk // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class SelectSweetener implements KioskHomeEvent { + const SelectSweetener(this.sweetener); + + + final String sweetener; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SelectSweetenerCopyWith get copyWith => _$SelectSweetenerCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SelectSweetener&&(identical(other.sweetener, sweetener) || other.sweetener == sweetener)); +} + + +@override +int get hashCode => Object.hash(runtimeType,sweetener); + +@override +String toString() { + return 'KioskHomeEvent.selectSweetener(sweetener: $sweetener)'; +} + + +} + +/// @nodoc +abstract mixin class $SelectSweetenerCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $SelectSweetenerCopyWith(SelectSweetener value, $Res Function(SelectSweetener) _then) = _$SelectSweetenerCopyWithImpl; +@useResult +$Res call({ + String sweetener +}); + + + + +} +/// @nodoc +class _$SelectSweetenerCopyWithImpl<$Res> + implements $SelectSweetenerCopyWith<$Res> { + _$SelectSweetenerCopyWithImpl(this._self, this._then); + + final SelectSweetener _self; + final $Res Function(SelectSweetener) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? sweetener = null,}) { + return _then(SelectSweetener( +null == sweetener ? _self.sweetener : sweetener // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class SubmitMilkAndSweetener implements KioskHomeEvent { + const SubmitMilkAndSweetener(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SubmitMilkAndSweetener); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent.submitMilkAndSweetener()'; +} + + +} + + + + +/// @nodoc + + +class SubmitHappyPlace implements KioskHomeEvent { + const SubmitHappyPlace(this.happyPlace); + + + final String happyPlace; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SubmitHappyPlaceCopyWith get copyWith => _$SubmitHappyPlaceCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SubmitHappyPlace&&(identical(other.happyPlace, happyPlace) || other.happyPlace == happyPlace)); +} + + +@override +int get hashCode => Object.hash(runtimeType,happyPlace); + +@override +String toString() { + return 'KioskHomeEvent.submitHappyPlace(happyPlace: $happyPlace)'; +} + + +} + +/// @nodoc +abstract mixin class $SubmitHappyPlaceCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $SubmitHappyPlaceCopyWith(SubmitHappyPlace value, $Res Function(SubmitHappyPlace) _then) = _$SubmitHappyPlaceCopyWithImpl; +@useResult +$Res call({ + String happyPlace +}); + + + + +} +/// @nodoc +class _$SubmitHappyPlaceCopyWithImpl<$Res> + implements $SubmitHappyPlaceCopyWith<$Res> { + _$SubmitHappyPlaceCopyWithImpl(this._self, this._then); + + final SubmitHappyPlace _self; + final $Res Function(SubmitHappyPlace) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? happyPlace = null,}) { + return _then(SubmitHappyPlace( +null == happyPlace ? _self.happyPlace : happyPlace // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class HappyPlaceRejected implements KioskHomeEvent { + const HappyPlaceRejected(this.reason); + + + final String reason; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$HappyPlaceRejectedCopyWith get copyWith => _$HappyPlaceRejectedCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is HappyPlaceRejected&&(identical(other.reason, reason) || other.reason == reason)); +} + + +@override +int get hashCode => Object.hash(runtimeType,reason); + +@override +String toString() { + return 'KioskHomeEvent.happyPlaceRejected(reason: $reason)'; +} + + +} + +/// @nodoc +abstract mixin class $HappyPlaceRejectedCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $HappyPlaceRejectedCopyWith(HappyPlaceRejected value, $Res Function(HappyPlaceRejected) _then) = _$HappyPlaceRejectedCopyWithImpl; +@useResult +$Res call({ + String reason +}); + + + + +} +/// @nodoc +class _$HappyPlaceRejectedCopyWithImpl<$Res> + implements $HappyPlaceRejectedCopyWith<$Res> { + _$HappyPlaceRejectedCopyWithImpl(this._self, this._then); + + final HappyPlaceRejected _self; + final $Res Function(HappyPlaceRejected) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? reason = null,}) { + return _then(HappyPlaceRejected( +null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class HappyPlaceModerationReasonShown implements KioskHomeEvent { + const HappyPlaceModerationReasonShown(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is HappyPlaceModerationReasonShown); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent.happyPlaceModerationReasonShown()'; +} + + +} + + + + +/// @nodoc + + +class ServerOrderUpdate implements KioskHomeEvent { + const ServerOrderUpdate(this.order); + + + final LatteOrder? order; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ServerOrderUpdateCopyWith get copyWith => _$ServerOrderUpdateCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerOrderUpdate&&(identical(other.order, order) || other.order == order)); +} + + +@override +int get hashCode => Object.hash(runtimeType,order); + +@override +String toString() { + return 'KioskHomeEvent.serverOrderUpdate(order: $order)'; +} + + +} + +/// @nodoc +abstract mixin class $ServerOrderUpdateCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $ServerOrderUpdateCopyWith(ServerOrderUpdate value, $Res Function(ServerOrderUpdate) _then) = _$ServerOrderUpdateCopyWithImpl; +@useResult +$Res call({ + LatteOrder? order +}); + + +$LatteOrderCopyWith<$Res>? get order; + +} +/// @nodoc +class _$ServerOrderUpdateCopyWithImpl<$Res> + implements $ServerOrderUpdateCopyWith<$Res> { + _$ServerOrderUpdateCopyWithImpl(this._self, this._then); + + final ServerOrderUpdate _self; + final $Res Function(ServerOrderUpdate) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? order = freezed,}) { + return _then(ServerOrderUpdate( +freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as LatteOrder?, + )); +} + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderCopyWith<$Res>? get order { + if (_self.order == null) { + return null; + } + + return $LatteOrderCopyWith<$Res>(_self.order!, (value) { + return _then(_self.copyWith(order: value)); + }); +} +} + +/// @nodoc + + +class ServerMetadataUpdate implements KioskHomeEvent { + const ServerMetadataUpdate(this.metadata); + + + final LatteOrderMetadata? metadata; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ServerMetadataUpdateCopyWith get copyWith => _$ServerMetadataUpdateCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerMetadataUpdate&&(identical(other.metadata, metadata) || other.metadata == metadata)); +} + + +@override +int get hashCode => Object.hash(runtimeType,metadata); + +@override +String toString() { + return 'KioskHomeEvent.serverMetadataUpdate(metadata: $metadata)'; +} + + +} + +/// @nodoc +abstract mixin class $ServerMetadataUpdateCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $ServerMetadataUpdateCopyWith(ServerMetadataUpdate value, $Res Function(ServerMetadataUpdate) _then) = _$ServerMetadataUpdateCopyWithImpl; +@useResult +$Res call({ + LatteOrderMetadata? metadata +}); + + +$LatteOrderMetadataCopyWith<$Res>? get metadata; + +} +/// @nodoc +class _$ServerMetadataUpdateCopyWithImpl<$Res> + implements $ServerMetadataUpdateCopyWith<$Res> { + _$ServerMetadataUpdateCopyWithImpl(this._self, this._then); + + final ServerMetadataUpdate _self; + final $Res Function(ServerMetadataUpdate) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? metadata = freezed,}) { + return _then(ServerMetadataUpdate( +freezed == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable +as LatteOrderMetadata?, + )); +} + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderMetadataCopyWith<$Res>? get metadata { + if (_self.metadata == null) { + return null; + } + + return $LatteOrderMetadataCopyWith<$Res>(_self.metadata!, (value) { + return _then(_self.copyWith(metadata: value)); + }); +} +} + +/// @nodoc + + +class GoBackKioskWizard implements KioskHomeEvent { + const GoBackKioskWizard(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is GoBackKioskWizard); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent.goBack()'; +} + + +} + + + + +/// @nodoc + + +class GoToStep implements KioskHomeEvent { + const GoToStep(this.step); + + + final KioskWizardStep step; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$GoToStepCopyWith get copyWith => _$GoToStepCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is GoToStep&&(identical(other.step, step) || other.step == step)); +} + + +@override +int get hashCode => Object.hash(runtimeType,step); + +@override +String toString() { + return 'KioskHomeEvent.goToStep(step: $step)'; +} + + +} + +/// @nodoc +abstract mixin class $GoToStepCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $GoToStepCopyWith(GoToStep value, $Res Function(GoToStep) _then) = _$GoToStepCopyWithImpl; +@useResult +$Res call({ + KioskWizardStep step +}); + + + + +} +/// @nodoc +class _$GoToStepCopyWithImpl<$Res> + implements $GoToStepCopyWith<$Res> { + _$GoToStepCopyWithImpl(this._self, this._then); + + final GoToStep _self; + final $Res Function(GoToStep) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? step = null,}) { + return _then(GoToStep( +null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as KioskWizardStep, + )); +} + + +} + +/// @nodoc + + +class SubmitUserName implements KioskHomeEvent { + const SubmitUserName(this.name); + + + final String name; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SubmitUserNameCopyWith get copyWith => _$SubmitUserNameCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SubmitUserName&&(identical(other.name, name) || other.name == name)); +} + + +@override +int get hashCode => Object.hash(runtimeType,name); + +@override +String toString() { + return 'KioskHomeEvent.submitUserName(name: $name)'; +} + + +} + +/// @nodoc +abstract mixin class $SubmitUserNameCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $SubmitUserNameCopyWith(SubmitUserName value, $Res Function(SubmitUserName) _then) = _$SubmitUserNameCopyWithImpl; +@useResult +$Res call({ + String name +}); + + + + +} +/// @nodoc +class _$SubmitUserNameCopyWithImpl<$Res> + implements $SubmitUserNameCopyWith<$Res> { + _$SubmitUserNameCopyWithImpl(this._self, this._then); + + final SubmitUserName _self; + final $Res Function(SubmitUserName) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? name = null,}) { + return _then(SubmitUserName( +null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class SelectImage implements KioskHomeEvent { + const SelectImage(this.index); + + + final int index; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SelectImageCopyWith get copyWith => _$SelectImageCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SelectImage&&(identical(other.index, index) || other.index == index)); +} + + +@override +int get hashCode => Object.hash(runtimeType,index); + +@override +String toString() { + return 'KioskHomeEvent.selectImage(index: $index)'; +} + + +} + +/// @nodoc +abstract mixin class $SelectImageCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $SelectImageCopyWith(SelectImage value, $Res Function(SelectImage) _then) = _$SelectImageCopyWithImpl; +@useResult +$Res call({ + int index +}); + + + + +} +/// @nodoc +class _$SelectImageCopyWithImpl<$Res> + implements $SelectImageCopyWith<$Res> { + _$SelectImageCopyWithImpl(this._self, this._then); + + final SelectImage _self; + final $Res Function(SelectImage) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? index = null,}) { + return _then(SelectImage( +null == index ? _self.index : index // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +/// @nodoc + + +class UpdateLatteImages implements KioskHomeEvent { + const UpdateLatteImages(this.batch); + + + final LatteImageBatch? batch; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$UpdateLatteImagesCopyWith get copyWith => _$UpdateLatteImagesCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdateLatteImages&&(identical(other.batch, batch) || other.batch == batch)); +} + + +@override +int get hashCode => Object.hash(runtimeType,batch); + +@override +String toString() { + return 'KioskHomeEvent.updateLatteImages(batch: $batch)'; +} + + +} + +/// @nodoc +abstract mixin class $UpdateLatteImagesCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $UpdateLatteImagesCopyWith(UpdateLatteImages value, $Res Function(UpdateLatteImages) _then) = _$UpdateLatteImagesCopyWithImpl; +@useResult +$Res call({ + LatteImageBatch? batch +}); + + +$LatteImageBatchCopyWith<$Res>? get batch; + +} +/// @nodoc +class _$UpdateLatteImagesCopyWithImpl<$Res> + implements $UpdateLatteImagesCopyWith<$Res> { + _$UpdateLatteImagesCopyWithImpl(this._self, this._then); + + final UpdateLatteImages _self; + final $Res Function(UpdateLatteImages) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? batch = freezed,}) { + return _then(UpdateLatteImages( +freezed == batch ? _self.batch : batch // ignore: cast_nullable_to_non_nullable +as LatteImageBatch?, + )); +} + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageBatchCopyWith<$Res>? get batch { + if (_self.batch == null) { + return null; + } + + return $LatteImageBatchCopyWith<$Res>(_self.batch!, (value) { + return _then(_self.copyWith(batch: value)); + }); +} +} + +/// @nodoc + + +class NewImageLatteBatch implements KioskHomeEvent { + const NewImageLatteBatch(this.latteImageBatchId); + + + final String latteImageBatchId; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NewImageLatteBatchCopyWith get copyWith => _$NewImageLatteBatchCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NewImageLatteBatch&&(identical(other.latteImageBatchId, latteImageBatchId) || other.latteImageBatchId == latteImageBatchId)); +} + + +@override +int get hashCode => Object.hash(runtimeType,latteImageBatchId); + +@override +String toString() { + return 'KioskHomeEvent.newImageLatteBatch(latteImageBatchId: $latteImageBatchId)'; +} + + +} + +/// @nodoc +abstract mixin class $NewImageLatteBatchCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $NewImageLatteBatchCopyWith(NewImageLatteBatch value, $Res Function(NewImageLatteBatch) _then) = _$NewImageLatteBatchCopyWithImpl; +@useResult +$Res call({ + String latteImageBatchId +}); + + + + +} +/// @nodoc +class _$NewImageLatteBatchCopyWithImpl<$Res> + implements $NewImageLatteBatchCopyWith<$Res> { + _$NewImageLatteBatchCopyWithImpl(this._self, this._then); + + final NewImageLatteBatch _self; + final $Res Function(NewImageLatteBatch) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? latteImageBatchId = null,}) { + return _then(NewImageLatteBatch( +null == latteImageBatchId ? _self.latteImageBatchId : latteImageBatchId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class UpdateQuestions implements KioskHomeEvent { + const UpdateQuestions(final List questions): _questions = questions; + + + final List _questions; + List get questions { + if (_questions is EqualUnmodifiableListView) return _questions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_questions); +} + + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$UpdateQuestionsCopyWith get copyWith => _$UpdateQuestionsCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdateQuestions&&const DeepCollectionEquality().equals(other._questions, _questions)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_questions)); + +@override +String toString() { + return 'KioskHomeEvent.updateQuestions(questions: $questions)'; +} + + +} + +/// @nodoc +abstract mixin class $UpdateQuestionsCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $UpdateQuestionsCopyWith(UpdateQuestions value, $Res Function(UpdateQuestions) _then) = _$UpdateQuestionsCopyWithImpl; +@useResult +$Res call({ + List questions +}); + + + + +} +/// @nodoc +class _$UpdateQuestionsCopyWithImpl<$Res> + implements $UpdateQuestionsCopyWith<$Res> { + _$UpdateQuestionsCopyWithImpl(this._self, this._then); + + final UpdateQuestions _self; + final $Res Function(UpdateQuestions) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? questions = null,}) { + return _then(UpdateQuestions( +null == questions ? _self._questions : questions // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class AnswerQuestion implements KioskHomeEvent { + const AnswerQuestion(this.question, this.answer); + + + final Question question; + final Object? answer; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AnswerQuestionCopyWith get copyWith => _$AnswerQuestionCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AnswerQuestion&&(identical(other.question, question) || other.question == question)&&const DeepCollectionEquality().equals(other.answer, answer)); +} + + +@override +int get hashCode => Object.hash(runtimeType,question,const DeepCollectionEquality().hash(answer)); + +@override +String toString() { + return 'KioskHomeEvent.answerQuestion(question: $question, answer: $answer)'; +} + + +} + +/// @nodoc +abstract mixin class $AnswerQuestionCopyWith<$Res> implements $KioskHomeEventCopyWith<$Res> { + factory $AnswerQuestionCopyWith(AnswerQuestion value, $Res Function(AnswerQuestion) _then) = _$AnswerQuestionCopyWithImpl; +@useResult +$Res call({ + Question question, Object? answer +}); + + +$QuestionCopyWith<$Res> get question; + +} +/// @nodoc +class _$AnswerQuestionCopyWithImpl<$Res> + implements $AnswerQuestionCopyWith<$Res> { + _$AnswerQuestionCopyWithImpl(this._self, this._then); + + final AnswerQuestion _self; + final $Res Function(AnswerQuestion) _then; + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? question = null,Object? answer = freezed,}) { + return _then(AnswerQuestion( +null == question ? _self.question : question // ignore: cast_nullable_to_non_nullable +as Question,freezed == answer ? _self.answer : answer , + )); +} + +/// Create a copy of KioskHomeEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$QuestionCopyWith<$Res> get question { + + return $QuestionCopyWith<$Res>(_self.question, (value) { + return _then(_self.copyWith(question: value)); + }); +} +} + +/// @nodoc + + +class GenerateRevisedImages implements KioskHomeEvent { + const GenerateRevisedImages(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is GenerateRevisedImages); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent.generateRevisedImages()'; +} + + +} + + + + +/// @nodoc + + +class RejectImageBatch implements KioskHomeEvent { + const RejectImageBatch(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RejectImageBatch); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent.rejectImageBatch()'; +} + + +} + + + + +/// @nodoc + + +class AcceptImage implements KioskHomeEvent { + const AcceptImage(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AcceptImage); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent.acceptImage()'; +} + + +} + + + + +/// @nodoc + + +class SubmitOrder implements KioskHomeEvent { + const SubmitOrder(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SubmitOrder); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'KioskHomeEvent.submitOrder()'; +} + + +} + + + + +/// @nodoc +mixin _$KioskHomeState { + + KioskWizardStep get currentStep; LatteOrder? get order; LatteOrderMetadata? get metadata; ModerationEvent? get happyPlaceModerationEvent; bool get isSubmitting; bool get isReverting;/// When the user submits a happy place, or a tweak, +/// the clock starts ticking on image generation. +/// We want to track this time. If for nothing else, it helps us show +/// a progress bar that doesn't depend on widget states being mounted +/// throughout the image generation without gaps. + DateTime? get imageGenerationStartTime;/// When an image's questions are requested before they exist, we store the +/// need here so that we add them to the state once they arrive. + bool get awaitingQuestions;/// The current batch of 4 images to choose from. This is deduced by the +/// [LatteOrderMetadata]'s `latteBatchId` field. + LatteImageBatch? get imagesBatch;/// Copy of the active [LatteImage]'s questions, only set once the user +/// chooses an image and clicks the "tweak image" button. This copy is used +/// instead of the questions in the [LatteImage] to isolate the user's +/// answers from changes to the [LatteImageBatch]. The critical detail is +/// that answers are not persisted to Firebase, which means anything that +/// overwrites the [LatteImageBatch] would clobber the user's answers. + List get questions;/// Values loaded straight from Firebase. +@LatteOptionConverter() List? get milkOptions;/// Values loaded straight from Firebase. +@LatteOptionConverter() List? get sweetenerOptions;/// True if the user is starting over and we want to eject data upon +/// returning to the .intro screen. + bool get shouldClearData; int? get selectedImageIndex; +/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$KioskHomeStateCopyWith get copyWith => _$KioskHomeStateCopyWithImpl(this as KioskHomeState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is KioskHomeState&&(identical(other.currentStep, currentStep) || other.currentStep == currentStep)&&(identical(other.order, order) || other.order == order)&&(identical(other.metadata, metadata) || other.metadata == metadata)&&(identical(other.happyPlaceModerationEvent, happyPlaceModerationEvent) || other.happyPlaceModerationEvent == happyPlaceModerationEvent)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.isReverting, isReverting) || other.isReverting == isReverting)&&(identical(other.imageGenerationStartTime, imageGenerationStartTime) || other.imageGenerationStartTime == imageGenerationStartTime)&&(identical(other.awaitingQuestions, awaitingQuestions) || other.awaitingQuestions == awaitingQuestions)&&(identical(other.imagesBatch, imagesBatch) || other.imagesBatch == imagesBatch)&&const DeepCollectionEquality().equals(other.questions, questions)&&const DeepCollectionEquality().equals(other.milkOptions, milkOptions)&&const DeepCollectionEquality().equals(other.sweetenerOptions, sweetenerOptions)&&(identical(other.shouldClearData, shouldClearData) || other.shouldClearData == shouldClearData)&&(identical(other.selectedImageIndex, selectedImageIndex) || other.selectedImageIndex == selectedImageIndex)); +} + + +@override +int get hashCode => Object.hash(runtimeType,currentStep,order,metadata,happyPlaceModerationEvent,isSubmitting,isReverting,imageGenerationStartTime,awaitingQuestions,imagesBatch,const DeepCollectionEquality().hash(questions),const DeepCollectionEquality().hash(milkOptions),const DeepCollectionEquality().hash(sweetenerOptions),shouldClearData,selectedImageIndex); + +@override +String toString() { + return 'KioskHomeState(currentStep: $currentStep, order: $order, metadata: $metadata, happyPlaceModerationEvent: $happyPlaceModerationEvent, isSubmitting: $isSubmitting, isReverting: $isReverting, imageGenerationStartTime: $imageGenerationStartTime, awaitingQuestions: $awaitingQuestions, imagesBatch: $imagesBatch, questions: $questions, milkOptions: $milkOptions, sweetenerOptions: $sweetenerOptions, shouldClearData: $shouldClearData, selectedImageIndex: $selectedImageIndex)'; +} + + +} + +/// @nodoc +abstract mixin class $KioskHomeStateCopyWith<$Res> { + factory $KioskHomeStateCopyWith(KioskHomeState value, $Res Function(KioskHomeState) _then) = _$KioskHomeStateCopyWithImpl; +@useResult +$Res call({ + KioskWizardStep currentStep, LatteOrder? order, LatteOrderMetadata? metadata, ModerationEvent? happyPlaceModerationEvent, bool isSubmitting, bool isReverting, DateTime? imageGenerationStartTime, bool awaitingQuestions, LatteImageBatch? imagesBatch, List questions,@LatteOptionConverter() List? milkOptions,@LatteOptionConverter() List? sweetenerOptions, bool shouldClearData, int? selectedImageIndex +}); + + +$LatteOrderCopyWith<$Res>? get order;$LatteOrderMetadataCopyWith<$Res>? get metadata;$LatteImageBatchCopyWith<$Res>? get imagesBatch; + +} +/// @nodoc +class _$KioskHomeStateCopyWithImpl<$Res> + implements $KioskHomeStateCopyWith<$Res> { + _$KioskHomeStateCopyWithImpl(this._self, this._then); + + final KioskHomeState _self; + final $Res Function(KioskHomeState) _then; + +/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? currentStep = null,Object? order = freezed,Object? metadata = freezed,Object? happyPlaceModerationEvent = freezed,Object? isSubmitting = null,Object? isReverting = null,Object? imageGenerationStartTime = freezed,Object? awaitingQuestions = null,Object? imagesBatch = freezed,Object? questions = null,Object? milkOptions = freezed,Object? sweetenerOptions = freezed,Object? shouldClearData = null,Object? selectedImageIndex = freezed,}) { + return _then(_self.copyWith( +currentStep: null == currentStep ? _self.currentStep : currentStep // ignore: cast_nullable_to_non_nullable +as KioskWizardStep,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as LatteOrder?,metadata: freezed == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable +as LatteOrderMetadata?,happyPlaceModerationEvent: freezed == happyPlaceModerationEvent ? _self.happyPlaceModerationEvent : happyPlaceModerationEvent // ignore: cast_nullable_to_non_nullable +as ModerationEvent?,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable +as bool,isReverting: null == isReverting ? _self.isReverting : isReverting // ignore: cast_nullable_to_non_nullable +as bool,imageGenerationStartTime: freezed == imageGenerationStartTime ? _self.imageGenerationStartTime : imageGenerationStartTime // ignore: cast_nullable_to_non_nullable +as DateTime?,awaitingQuestions: null == awaitingQuestions ? _self.awaitingQuestions : awaitingQuestions // ignore: cast_nullable_to_non_nullable +as bool,imagesBatch: freezed == imagesBatch ? _self.imagesBatch : imagesBatch // ignore: cast_nullable_to_non_nullable +as LatteImageBatch?,questions: null == questions ? _self.questions : questions // ignore: cast_nullable_to_non_nullable +as List,milkOptions: freezed == milkOptions ? _self.milkOptions : milkOptions // ignore: cast_nullable_to_non_nullable +as List?,sweetenerOptions: freezed == sweetenerOptions ? _self.sweetenerOptions : sweetenerOptions // ignore: cast_nullable_to_non_nullable +as List?,shouldClearData: null == shouldClearData ? _self.shouldClearData : shouldClearData // ignore: cast_nullable_to_non_nullable +as bool,selectedImageIndex: freezed == selectedImageIndex ? _self.selectedImageIndex : selectedImageIndex // ignore: cast_nullable_to_non_nullable +as int?, + )); +} +/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderCopyWith<$Res>? get order { + if (_self.order == null) { + return null; + } + + return $LatteOrderCopyWith<$Res>(_self.order!, (value) { + return _then(_self.copyWith(order: value)); + }); +}/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderMetadataCopyWith<$Res>? get metadata { + if (_self.metadata == null) { + return null; + } + + return $LatteOrderMetadataCopyWith<$Res>(_self.metadata!, (value) { + return _then(_self.copyWith(metadata: value)); + }); +}/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageBatchCopyWith<$Res>? get imagesBatch { + if (_self.imagesBatch == null) { + return null; + } + + return $LatteImageBatchCopyWith<$Res>(_self.imagesBatch!, (value) { + return _then(_self.copyWith(imagesBatch: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [KioskHomeState]. +extension KioskHomeStatePatterns on KioskHomeState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _KioskHomeState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _KioskHomeState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _KioskHomeState value) $default,){ +final _that = this; +switch (_that) { +case _KioskHomeState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _KioskHomeState value)? $default,){ +final _that = this; +switch (_that) { +case _KioskHomeState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( KioskWizardStep currentStep, LatteOrder? order, LatteOrderMetadata? metadata, ModerationEvent? happyPlaceModerationEvent, bool isSubmitting, bool isReverting, DateTime? imageGenerationStartTime, bool awaitingQuestions, LatteImageBatch? imagesBatch, List questions, @LatteOptionConverter() List? milkOptions, @LatteOptionConverter() List? sweetenerOptions, bool shouldClearData, int? selectedImageIndex)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _KioskHomeState() when $default != null: +return $default(_that.currentStep,_that.order,_that.metadata,_that.happyPlaceModerationEvent,_that.isSubmitting,_that.isReverting,_that.imageGenerationStartTime,_that.awaitingQuestions,_that.imagesBatch,_that.questions,_that.milkOptions,_that.sweetenerOptions,_that.shouldClearData,_that.selectedImageIndex);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( KioskWizardStep currentStep, LatteOrder? order, LatteOrderMetadata? metadata, ModerationEvent? happyPlaceModerationEvent, bool isSubmitting, bool isReverting, DateTime? imageGenerationStartTime, bool awaitingQuestions, LatteImageBatch? imagesBatch, List questions, @LatteOptionConverter() List? milkOptions, @LatteOptionConverter() List? sweetenerOptions, bool shouldClearData, int? selectedImageIndex) $default,) {final _that = this; +switch (_that) { +case _KioskHomeState(): +return $default(_that.currentStep,_that.order,_that.metadata,_that.happyPlaceModerationEvent,_that.isSubmitting,_that.isReverting,_that.imageGenerationStartTime,_that.awaitingQuestions,_that.imagesBatch,_that.questions,_that.milkOptions,_that.sweetenerOptions,_that.shouldClearData,_that.selectedImageIndex);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( KioskWizardStep currentStep, LatteOrder? order, LatteOrderMetadata? metadata, ModerationEvent? happyPlaceModerationEvent, bool isSubmitting, bool isReverting, DateTime? imageGenerationStartTime, bool awaitingQuestions, LatteImageBatch? imagesBatch, List questions, @LatteOptionConverter() List? milkOptions, @LatteOptionConverter() List? sweetenerOptions, bool shouldClearData, int? selectedImageIndex)? $default,) {final _that = this; +switch (_that) { +case _KioskHomeState() when $default != null: +return $default(_that.currentStep,_that.order,_that.metadata,_that.happyPlaceModerationEvent,_that.isSubmitting,_that.isReverting,_that.imageGenerationStartTime,_that.awaitingQuestions,_that.imagesBatch,_that.questions,_that.milkOptions,_that.sweetenerOptions,_that.shouldClearData,_that.selectedImageIndex);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _KioskHomeState extends KioskHomeState { + const _KioskHomeState({required this.currentStep, this.order, this.metadata, this.happyPlaceModerationEvent, this.isSubmitting = false, this.isReverting = false, this.imageGenerationStartTime, this.awaitingQuestions = false, this.imagesBatch, final List questions = const [], @LatteOptionConverter() final List? milkOptions, @LatteOptionConverter() final List? sweetenerOptions, this.shouldClearData = false, this.selectedImageIndex}): _questions = questions,_milkOptions = milkOptions,_sweetenerOptions = sweetenerOptions,super._(); + + +@override final KioskWizardStep currentStep; +@override final LatteOrder? order; +@override final LatteOrderMetadata? metadata; +@override final ModerationEvent? happyPlaceModerationEvent; +@override@JsonKey() final bool isSubmitting; +@override@JsonKey() final bool isReverting; +/// When the user submits a happy place, or a tweak, +/// the clock starts ticking on image generation. +/// We want to track this time. If for nothing else, it helps us show +/// a progress bar that doesn't depend on widget states being mounted +/// throughout the image generation without gaps. +@override final DateTime? imageGenerationStartTime; +/// When an image's questions are requested before they exist, we store the +/// need here so that we add them to the state once they arrive. +@override@JsonKey() final bool awaitingQuestions; +/// The current batch of 4 images to choose from. This is deduced by the +/// [LatteOrderMetadata]'s `latteBatchId` field. +@override final LatteImageBatch? imagesBatch; +/// Copy of the active [LatteImage]'s questions, only set once the user +/// chooses an image and clicks the "tweak image" button. This copy is used +/// instead of the questions in the [LatteImage] to isolate the user's +/// answers from changes to the [LatteImageBatch]. The critical detail is +/// that answers are not persisted to Firebase, which means anything that +/// overwrites the [LatteImageBatch] would clobber the user's answers. + final List _questions; +/// Copy of the active [LatteImage]'s questions, only set once the user +/// chooses an image and clicks the "tweak image" button. This copy is used +/// instead of the questions in the [LatteImage] to isolate the user's +/// answers from changes to the [LatteImageBatch]. The critical detail is +/// that answers are not persisted to Firebase, which means anything that +/// overwrites the [LatteImageBatch] would clobber the user's answers. +@override@JsonKey() List get questions { + if (_questions is EqualUnmodifiableListView) return _questions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_questions); +} + +/// Values loaded straight from Firebase. + final List? _milkOptions; +/// Values loaded straight from Firebase. +@override@LatteOptionConverter() List? get milkOptions { + final value = _milkOptions; + if (value == null) return null; + if (_milkOptions is EqualUnmodifiableListView) return _milkOptions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + +/// Values loaded straight from Firebase. + final List? _sweetenerOptions; +/// Values loaded straight from Firebase. +@override@LatteOptionConverter() List? get sweetenerOptions { + final value = _sweetenerOptions; + if (value == null) return null; + if (_sweetenerOptions is EqualUnmodifiableListView) return _sweetenerOptions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + +/// True if the user is starting over and we want to eject data upon +/// returning to the .intro screen. +@override@JsonKey() final bool shouldClearData; +@override final int? selectedImageIndex; + +/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$KioskHomeStateCopyWith<_KioskHomeState> get copyWith => __$KioskHomeStateCopyWithImpl<_KioskHomeState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _KioskHomeState&&(identical(other.currentStep, currentStep) || other.currentStep == currentStep)&&(identical(other.order, order) || other.order == order)&&(identical(other.metadata, metadata) || other.metadata == metadata)&&(identical(other.happyPlaceModerationEvent, happyPlaceModerationEvent) || other.happyPlaceModerationEvent == happyPlaceModerationEvent)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.isReverting, isReverting) || other.isReverting == isReverting)&&(identical(other.imageGenerationStartTime, imageGenerationStartTime) || other.imageGenerationStartTime == imageGenerationStartTime)&&(identical(other.awaitingQuestions, awaitingQuestions) || other.awaitingQuestions == awaitingQuestions)&&(identical(other.imagesBatch, imagesBatch) || other.imagesBatch == imagesBatch)&&const DeepCollectionEquality().equals(other._questions, _questions)&&const DeepCollectionEquality().equals(other._milkOptions, _milkOptions)&&const DeepCollectionEquality().equals(other._sweetenerOptions, _sweetenerOptions)&&(identical(other.shouldClearData, shouldClearData) || other.shouldClearData == shouldClearData)&&(identical(other.selectedImageIndex, selectedImageIndex) || other.selectedImageIndex == selectedImageIndex)); +} + + +@override +int get hashCode => Object.hash(runtimeType,currentStep,order,metadata,happyPlaceModerationEvent,isSubmitting,isReverting,imageGenerationStartTime,awaitingQuestions,imagesBatch,const DeepCollectionEquality().hash(_questions),const DeepCollectionEquality().hash(_milkOptions),const DeepCollectionEquality().hash(_sweetenerOptions),shouldClearData,selectedImageIndex); + +@override +String toString() { + return 'KioskHomeState(currentStep: $currentStep, order: $order, metadata: $metadata, happyPlaceModerationEvent: $happyPlaceModerationEvent, isSubmitting: $isSubmitting, isReverting: $isReverting, imageGenerationStartTime: $imageGenerationStartTime, awaitingQuestions: $awaitingQuestions, imagesBatch: $imagesBatch, questions: $questions, milkOptions: $milkOptions, sweetenerOptions: $sweetenerOptions, shouldClearData: $shouldClearData, selectedImageIndex: $selectedImageIndex)'; +} + + +} + +/// @nodoc +abstract mixin class _$KioskHomeStateCopyWith<$Res> implements $KioskHomeStateCopyWith<$Res> { + factory _$KioskHomeStateCopyWith(_KioskHomeState value, $Res Function(_KioskHomeState) _then) = __$KioskHomeStateCopyWithImpl; +@override @useResult +$Res call({ + KioskWizardStep currentStep, LatteOrder? order, LatteOrderMetadata? metadata, ModerationEvent? happyPlaceModerationEvent, bool isSubmitting, bool isReverting, DateTime? imageGenerationStartTime, bool awaitingQuestions, LatteImageBatch? imagesBatch, List questions,@LatteOptionConverter() List? milkOptions,@LatteOptionConverter() List? sweetenerOptions, bool shouldClearData, int? selectedImageIndex +}); + + +@override $LatteOrderCopyWith<$Res>? get order;@override $LatteOrderMetadataCopyWith<$Res>? get metadata;@override $LatteImageBatchCopyWith<$Res>? get imagesBatch; + +} +/// @nodoc +class __$KioskHomeStateCopyWithImpl<$Res> + implements _$KioskHomeStateCopyWith<$Res> { + __$KioskHomeStateCopyWithImpl(this._self, this._then); + + final _KioskHomeState _self; + final $Res Function(_KioskHomeState) _then; + +/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? currentStep = null,Object? order = freezed,Object? metadata = freezed,Object? happyPlaceModerationEvent = freezed,Object? isSubmitting = null,Object? isReverting = null,Object? imageGenerationStartTime = freezed,Object? awaitingQuestions = null,Object? imagesBatch = freezed,Object? questions = null,Object? milkOptions = freezed,Object? sweetenerOptions = freezed,Object? shouldClearData = null,Object? selectedImageIndex = freezed,}) { + return _then(_KioskHomeState( +currentStep: null == currentStep ? _self.currentStep : currentStep // ignore: cast_nullable_to_non_nullable +as KioskWizardStep,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as LatteOrder?,metadata: freezed == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable +as LatteOrderMetadata?,happyPlaceModerationEvent: freezed == happyPlaceModerationEvent ? _self.happyPlaceModerationEvent : happyPlaceModerationEvent // ignore: cast_nullable_to_non_nullable +as ModerationEvent?,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable +as bool,isReverting: null == isReverting ? _self.isReverting : isReverting // ignore: cast_nullable_to_non_nullable +as bool,imageGenerationStartTime: freezed == imageGenerationStartTime ? _self.imageGenerationStartTime : imageGenerationStartTime // ignore: cast_nullable_to_non_nullable +as DateTime?,awaitingQuestions: null == awaitingQuestions ? _self.awaitingQuestions : awaitingQuestions // ignore: cast_nullable_to_non_nullable +as bool,imagesBatch: freezed == imagesBatch ? _self.imagesBatch : imagesBatch // ignore: cast_nullable_to_non_nullable +as LatteImageBatch?,questions: null == questions ? _self._questions : questions // ignore: cast_nullable_to_non_nullable +as List,milkOptions: freezed == milkOptions ? _self._milkOptions : milkOptions // ignore: cast_nullable_to_non_nullable +as List?,sweetenerOptions: freezed == sweetenerOptions ? _self._sweetenerOptions : sweetenerOptions // ignore: cast_nullable_to_non_nullable +as List?,shouldClearData: null == shouldClearData ? _self.shouldClearData : shouldClearData // ignore: cast_nullable_to_non_nullable +as bool,selectedImageIndex: freezed == selectedImageIndex ? _self.selectedImageIndex : selectedImageIndex // ignore: cast_nullable_to_non_nullable +as int?, + )); +} + +/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderCopyWith<$Res>? get order { + if (_self.order == null) { + return null; + } + + return $LatteOrderCopyWith<$Res>(_self.order!, (value) { + return _then(_self.copyWith(order: value)); + }); +}/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOrderMetadataCopyWith<$Res>? get metadata { + if (_self.metadata == null) { + return null; + } + + return $LatteOrderMetadataCopyWith<$Res>(_self.metadata!, (value) { + return _then(_self.copyWith(metadata: value)); + }); +}/// Create a copy of KioskHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteImageBatchCopyWith<$Res>? get imagesBatch { + if (_self.imagesBatch == null) { + return null; + } + + return $LatteImageBatchCopyWith<$Res>(_self.imagesBatch!, (value) { + return _then(_self.copyWith(imagesBatch: value)); + }); +} +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home_view.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home_view.dart new file mode 100644 index 0000000..d2e1f54 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/kiosk_home_view.dart @@ -0,0 +1,445 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/kiosk/home/kiosk_home.dart'; +import 'package:genlatte/src/screens/kiosk/home/steps/steps.dart'; +import 'package:genlatte/src/screens/kiosk/widgets/widgets.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// {@template KioskHomeScreen} +/// Initial Kiosk home screen. +/// {@endtemplate} +class KioskHomeScreen extends StatefulWidget { + /// {@macro KioskHomeScreen} + const KioskHomeScreen({ + super.key, + this.bloc, + }); + + /// Optional bloc, primarily used for testing. + final KioskHomeBloc? bloc; + + @override + State createState() => _KioskHomeScreenState(); +} + +class _KioskHomeScreenState extends State { + late final KioskHomeBloc bloc = widget.bloc ?? KioskHomeBloc(); + + final _shownHappyPlaceModerationIds = {}; + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, info) { + return BlocListener( + bloc: bloc, + listener: (context, state) async { + if (state.happyPlaceModerationEvent != null) { + // Short-circuit if we've already seen this moderation event. + if (_shownHappyPlaceModerationIds.contains( + state.happyPlaceModerationEvent!.id, + )) { + return; + } + + _shownHappyPlaceModerationIds.add( + state.happyPlaceModerationEvent!.id, + ); + bloc.add(const HappyPlaceModerationReasonShown()); + final message = state.happyPlaceModerationEvent!.reason + .capitalize(); + showToast( + context: context, + builder: (BuildContext context, ToastOverlay overlay) { + return SurfaceCard( + child: Basic( + title: const Text( + 'Happy Place rejected', + style: TextStyle(color: AppColors.googleIntroRed), + ).large, + subtitle: Text( + message, + style: const TextStyle(color: AppColors.black), + ).base, + trailing: OutlineButton( + // size: ButtonSize.small, + onPressed: overlay.close, + child: const Text( + 'OK', + style: TextStyle(color: AppColors.googleIntroRed), + ), + ), + trailingAlignment: Alignment.bottomCenter, + ), + ); + }, + location: ToastLocation.bottomCenter, + showDuration: const Duration(seconds: 10), + // onClosed: () {}, + ); + } + }, + child: BlocBuilder( + bloc: bloc, + builder: (context, state) { + return SafeArea( + child: Stack( + children: [ + Positioned.fill( + child: GenLatteScaffold( + headers: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeOut, + child: + // TODO(filiph): Use a less drastic measure + // to reclaim vertical space + // when keyboard is shown + (MediaQuery.viewInsetsOf(context).bottom == 0) + ? Padding( + padding: const EdgeInsets.only( + top: 48, + left: 12, + right: 12, + ), + child: Row( + mainAxisAlignment: .spaceBetween, + children: [ + const Spacer(), + Column( + children: [ + TripleTapDetector( + semanticLabel: 'GenLatte', + semanticHint: + 'Triple tap to reset ' + 'the GenLatte ' + 'ordering process', + onPressed: () async { + bloc.add(const StartOver()); + await GetIt.I + .get() + .logEvent( + name: 'kiosk_start_over', + ); + }, + child: const Text('GenLatte').h4, + ), + if (!state + .currentStep + .isFirstStep) ...[ + const SizedBox(height: 8), + SegmentedProgress( + currentStep: state + .currentStep + .progressIndicatorIndex, + totalSteps: 4, + ), + ], + if (state + .currentStep + .isFirstStep) ...[ + // 20 + 8, for the above sized box + // and the above active height + // of 20 + const SizedBox(height: 28), + ], + ], + ), + const Spacer(), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], + footers: [ + if (MediaQuery.viewInsetsOf(context).bottom == 0) + Padding( + padding: const EdgeInsets.symmetric(vertical: 48), + child: TripleTapDetector( + semanticLabel: 'Footer', + semanticHint: 'Triple tap to log out', + onPressed: () => + GetIt.I().signOut(), + child: Footer( + uiScale: max(1, info.width / 1080), + ), + ), + ), + ], + child: Padding( + padding: info.orientation.isPortrait + ? EdgeInsets.zero + : EdgeInsets.symmetric( + horizontal: (info.width * 0.05).clamp( + 12, + 24, + ), + ), + child: Center( + child: ZipPageView( + currentIndex: state.currentStep.stepIndex, + builder: (context, index) { + return buildStep(context, state, info, index); + }, + itemCount: KioskWizardStep.values.length, + onNewPage: (int newPageIndex) { + bloc.add( + KioskHomeEvent.onNewPage( + KioskWizardStep.fromIndex(newPageIndex), + ), + ); + }, + key: const ValueKey('zip-page-view'), + ), + ), + ), + ), + ), + if (!state.currentStep.isFirstStep && + !state.currentStep.isLastStep) + Positioned( + left: 32, + top: 32, + child: GenLatteFilledButton( + onPressed: () async { + bloc.add(const KioskHomeEvent.goBack()); + await GetIt.I.get().logEvent( + name: 'kiosk_go_back', + parameters: {'from': state.currentStep.name}, + ); + }, + label: info.orientation.isPortrait ? null : 'Back', + icon: Icons.arrow_back, + ), + ), + if (state.currentStep != .submitOrder && + state.currentStep != .confirmation && + state.metadata?.imageUrl != null) + Positioned( + right: 32, + top: 32, + child: GenLatteFilledButton( + onPressed: () => bloc.add( + const KioskHomeEvent.goToStep(.submitOrder), + ), + label: info.orientation.isPortrait ? null : 'Submit', + trailingIcon: Icons.arrow_forward, + ), + ), + ], + ), + ); + }, + ), + ); + }, + ); + } + + Widget buildStep( + BuildContext context, + KioskHomeState state, + LayoutInformation info, + int pageIndex, + ) { + final child = switch (KioskWizardStep.fromIndex(pageIndex)) { + .intro => KioskIntro( + key: const ValueKey('step-intro'), + advance: () { + bloc.add(const KioskHomeEvent.goToStep(.name)); + }, + ), + .name => KioskDrinkOrderName( + key: const Key('step-name'), + advance: !state.isSubmitting + ? (String name) async { + bloc.add(SubmitUserName(name)); + await GetIt.I.get().logEvent( + name: 'kiosk_submit_name', + ); + } + : null, + name: state.order?.name, + ), + .milkAndSweetener => KioskDrinkMilkSweetener( + key: const Key('step-milk-sweetener'), + advance: + !state.isSubmitting && + state.order?.milk != null && + state.order?.sweetener != null + ? () async { + bloc.add(const SubmitMilkAndSweetener()); + await GetIt.I.get().logEvent( + name: 'kiosk_submit_milk_sweetener', + parameters: { + 'milk': state.order?.milk ?? 'missing-milk-wtf', + 'sweetener': + state.order?.sweetener ?? 'missing-sweetener-wtf', + }, + ); + } + : null, + milkOptions: state.milkOptions, + selectedMilk: state.order?.milk, + selectedSweetener: state.order?.sweetener, + selectMilk: (milk) => bloc.add(SelectMilk(milk)), + selectSweetener: (sweetener) => bloc.add(SelectSweetener(sweetener)), + sweetenerOptions: state.sweetenerOptions, + username: state.order!.name!, + ), + .happyPlace => KioskHappyPlace( + key: const Key('step-happy-place'), + happyPlace: state.order!.happyPlace, + submitHappyPlace: !state.isSubmitting + ? (String happyPlace) async { + bloc.add(SubmitHappyPlace(happyPlace)); + await GetIt.I.get().logEvent( + name: 'kiosk_submit_happy_place', + ); + } + : null, + ), + .chooseAnImage => KioskChooseAnImage( + key: const Key('step-choose-image'), + imageGenerationStartTime: state.imageGenerationStartTime, + advanceToAcceptOrder: () async { + bloc.add(const AcceptImage()); + await GetIt.I.get().logEvent( + name: 'kiosk_accept_image', + ); + }, + advanceToTweakImage: () async { + bloc.add(const KioskHomeEvent.goToStep(.tweakImage)); + await GetIt.I.get().logEvent( + name: 'kiosk_tweak_image', + ); + }, + isSubmitting: state.isSubmitting, + imagesBatch: state.imagesBatch, + selectedIndex: state.selectedImageIndex, + onSelectImage: (int index) async { + bloc.add(KioskHomeEvent.selectImage(index)); + await GetIt.I.get().logEvent( + name: 'kiosk_select_image', + ); + }, + ), + .tweakImage => KioskTweakImage( + key: const Key('step-tweak-image'), + advance: + !state.isSubmitting && state.questions.every((q) => q.isAnswered) + ? () async { + bloc.add(const KioskHomeEvent.generateRevisedImages()); + await GetIt.I.get().logEvent( + name: 'kiosk_generate_revised_images', + ); + } + : null, + questions: state.questions.take(4).toList(), + onAnswer: (Question question, Object? answer) { + bloc.add(AnswerQuestion(question, answer)); + }, + ), + .chooseATweakedImage => KioskChooseAnImage( + key: const Key('step-choose-tweaked'), + imageGenerationStartTime: state.imageGenerationStartTime, + advanceToAcceptOrder: () async { + bloc.add(const AcceptImage()); + await GetIt.I.get().logEvent( + name: 'kiosk_accept_tweaked_image', + ); + }, + advanceToTweakImage: null, + imagesBatch: state.imagesBatch, + isSubmitting: state.isSubmitting, + selectedIndex: state.selectedImageIndex, + onSelectImage: (int index) async { + bloc.add(KioskHomeEvent.selectImage(index)); + await GetIt.I.get().logEvent( + name: 'kiosk_select_image', + ); + }, + ), + .submitOrder => KioskSubmitOrder( + key: const Key('step-submit-order'), + advance: !state.isSubmitting + ? () async { + bloc.add(const SubmitOrder()); + await GetIt.I.get().logEvent( + name: 'kiosk_submit_order', + ); + } + : null, + metadata: state.metadata!, + order: state.order!, + returnToLatteConfig: () async { + bloc.add(const KioskHomeEvent.goToStep(.milkAndSweetener)); + await GetIt.I.get().logEvent( + name: 'kiosk_return_to_latte_config', + ); + }, + ), + .confirmation => KioskConfirmation( + key: const Key('step-confirmation'), + onNewOrder: () => bloc.add(const StartOver()), + orderNumber: state.metadata!.orderNumber!, + ), + }; + + final Widget? header = switch (KioskWizardStep.fromIndex(pageIndex)) { + .intro || + .name || + .milkAndSweetener || + .happyPlace || + .tweakImage || + .submitOrder || + .confirmation => null, + .chooseAnImage => const KioskChooseAnImageHeader(), + .chooseATweakedImage => + state.imagesBatch != null + ? KioskChooseATweakedImageHeader( + isReverting: state.isReverting, + onRevert: !state.isSubmitting + ? () async { + bloc.add(const RejectImageBatch()); + await GetIt.I.get().logEvent( + name: 'kiosk_reject_image_batch', + ); + } + : null, + ) + : null, + }; + return KioskStep( + header: header, + child: child, + ); + } + + @override + void dispose() { + super.dispose(); + bloc.close().ignore(); + } +} + +extension on String { + /// Capitalizes the first letter of the string. + String capitalize() { + if (isEmpty) return this; + return '${this[0].toUpperCase()}${substring(1)}'; + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/CONTEXT.md new file mode 100644 index 0000000..9055145 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/CONTEXT.md @@ -0,0 +1,42 @@ +# Kiosk Wizard Steps + +**Purpose:** +Contains the constituent UI components for each individual step in the Kiosk ordering wizard. These components are orchestrated by `KioskHomeBloc` and stitched together visually by `ZipPageView`. + +**Detailed File Overviews:** + +- `kiosk_choose_an_image.dart`: + - **Description**: Displays the 4 initial image options generated by Nanobanana based on the user's happy place. + - **Core Logic**: Adjusts layout based on aspect ratio (`_LatteArtCarousel` vs `_LatteArtField`). Highlights selected images dynamically and offers options to either "Accept" a chosen image directly or "Tweak" it. + +- `kiosk_tweak_image.dart`: + - **Description**: The interface where the user answers AI-generated follow-up questions to iterate on a chosen image. + - **Core Logic**: Maps a dynamic list of `Question` records from the server to `ConfigurationCard` widgets using `Flavor` (to give different colors). Changes layout based on aspect ratio/count of questions. + +- `kiosk_happy_place.dart`: + - **Description**: A free-text input step where the user submits their happy place. + - **Core Logic**: Evaluates the local `TextEditingController` state to unblock the "Next" (Submit) button, and triggers `submitHappyPlace`. Beautifully styles the background with gradients and specific flutter-latte assets. + +- `kiosk_submit_order.dart`: + - **Description**: The final confirmation screen. + - **Core Logic**: Displays a recap of the entire order (Milk, Sweetener, Drink Type, Happy Place, and selected AI Image). Upon advancing, the full order is finalized. + +- `kiosk_drink_order_name.dart`: + - **Description**: A simple input step for the customer's name. + +- `kiosk_drink_milk_sweetener.dart`: + - **Description**: A two-question configuration screen for Milk (e.g. Whole, Skim, Oat) and Sweetener preferences using standard `ConfigurationCard` instances. + +- `kiosk_intro.dart`: + - **Description**: The landing state / starting point of the kiosk, introducing the application. + +- `kiosk_step.dart`: + - **Description**: Contains enum definition `KioskWizardStep` declaring the exact linear order of these screens. (May be imported from models or blocs, depending on specific mapping). + +- `steps.dart`: + - **Description**: Barrel export file exposing all steps. + +**Dependencies/Relationships:** +- Strictly consumed by `kiosk/home/kiosk_home_view.dart`. +- They all implement localized layout strategies based on screen aspect ratios (horizontal vs vertical layouts for tablet vs kiosk mode). +- They rely heavily on `kiosk/widgets/zip_item.dart` to integrate with the parent `ZipPageView` animations. diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_choose_an_image.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_choose_an_image.dart new file mode 100644 index 0000000..c5dfb22 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_choose_an_image.dart @@ -0,0 +1,699 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:genlatte/src/screens/app/app.dart' show AppColors; +import 'package:genlatte/src/screens/kiosk/widgets/loading_progress.dart'; +import 'package:genlatte/src/screens/kiosk/widgets/widgets.dart'; +import 'package:genlatte/src/screens/kiosk/widgets/zip_item.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:text_responsive/text_responsive.dart'; + +/// Header for the kiosk choose an image step. +class KioskChooseAnImageHeader extends StatelessWidget { + /// Instantiates a [KioskChooseAnImageHeader]. + const KioskChooseAnImageHeader({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.only(top: 8), + child: ParagraphTextWidget( + 'Which one of these is closest\nto what you have in mind?', + style: TextStyle(color: AppColors.white, fontSize: 20), + textAlign: .center, + ), + ); + } +} + +/// Revert button to sit above tweaked images. +class KioskChooseATweakedImageHeader extends StatelessWidget { + /// Instantiates a [KioskChooseATweakedImageHeader]. + const KioskChooseATweakedImageHeader({ + required this.isReverting, + required this.onRevert, + super.key, + }); + + /// Whether the user is reverting to their original images. + final bool isReverting; + + /// Callback to return a user to their original images. + final VoidCallback? onRevert; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Center( + child: GenLatteOutlinedButton.light( + onPressed: onRevert, + label: !isReverting ? 'Revert to originals' : 'Reverting...', + ), + ), + ); + } +} + +const _emptyImages = [null, null, null, null]; + +/// The step in the kiosk wizard where the user chooses an image from the pool +/// of 4 generated by Nanobanana. +class KioskChooseAnImage extends StatelessWidget { + /// Instantiates a [KioskChooseAnImage]. + const KioskChooseAnImage({ + required this.advanceToAcceptOrder, + required this.advanceToTweakImage, + required this.imagesBatch, + required this.isSubmitting, + required this.onSelectImage, + required this.selectedIndex, + required this.imageGenerationStartTime, + super.key, + }); + + /// Callback to invoke when the user skips to the accept order step. + final VoidCallback advanceToAcceptOrder; + + /// Callback to invoke when the user chooses an image. + final VoidCallback? advanceToTweakImage; + + /// Callback to invoke when the user selects an image. + final void Function(int) onSelectImage; + + /// Whether the user is submitting the order. + final bool isSubmitting; + + /// User-selected latte art index. + final int? selectedIndex; + + /// The images to display. + final LatteImageBatch? imagesBatch; + + /// The time when image generation process started. + /// + /// A note on nullability: When choosing an image, + /// the [imageGenerationStartTime] really shouldn't be null. + /// But because of shenanigans around hot restart versus loading state + /// from previous session, it's safer to assume nullable time + /// and deal with it below in the widget tree. + final DateTime? imageGenerationStartTime; + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, info) { + return switch (info.aspectRatio) { + < 1.6 => _LatteArtCarousel( + advanceToTweakImage: advanceToTweakImage, + advanceToAcceptOrder: advanceToAcceptOrder, + imagesBatch: imagesBatch, + layoutInfo: info, + isSubmitting: isSubmitting, + onSelectImage: onSelectImage, + selectedIndex: selectedIndex, + imageGenerationStartTime: imageGenerationStartTime, + ), + _ => _LatteArtField( + advanceToAcceptOrder: advanceToAcceptOrder, + advanceToTweakImage: advanceToTweakImage, + imagesBatch: imagesBatch, + layoutInfo: info, + isSubmitting: isSubmitting, + onSelectImage: onSelectImage, + selectedIndex: selectedIndex, + imageGenerationStartTime: imageGenerationStartTime, + ), + }; + }, + ); + } +} + +class _LatteArtCarousel extends StatelessWidget { + const _LatteArtCarousel({ + required this.advanceToAcceptOrder, + required this.advanceToTweakImage, + required this.imagesBatch, + required this.isSubmitting, + required this.layoutInfo, + required this.onSelectImage, + required this.selectedIndex, + required this.imageGenerationStartTime, + }); + + final VoidCallback advanceToAcceptOrder; + final VoidCallback? advanceToTweakImage; + + final LatteImageBatch? imagesBatch; + final LayoutInformation layoutInfo; + final bool isSubmitting; + final void Function(int) onSelectImage; + final int? selectedIndex; + final DateTime? imageGenerationStartTime; + + @override + Widget build(BuildContext context) { + final images = imagesBatch?.images.toList() ?? _emptyImages; + final realImagesCount = images.whereNotNull().length; + + final shouldShowButtons = realImagesCount > 0; + + return LayoutProvider.builder( + builder: (context, info) { + return Stack( + clipBehavior: .none, + fit: .expand, + children: [ + Positioned( + top: 0, + left: 0, + width: 0, + height: 0, + child: SizedBox.shrink( + // This is here for ease of testing (while also preventing + // a forced unmount of the whole widget tree). + key: ValueKey('step-choose-image-${imagesBatch?.id ?? 'none'}'), + ), + ), + Positioned.fill( + child: Column( + children: [ + const Spacer(), + Expanded( + flex: 9, + child: Center( + child: SneakPeakCarousel( + children: [ + ...images.indexed.map( + (imageAndIndex) { + if (images[imageAndIndex.$1] == null) { + return Padding( + padding: info.aspectRatio > 1 + ? EdgeInsets.symmetric( + vertical: info.height * 0.03, + ) + : EdgeInsets.zero, + child: EmptyLatteArt( + key: ValueKey(imageAndIndex.$1.toString()), + ), + ); + } + return LayoutBuilder( + builder: (context, constraints) => ZipItem( + index: 1, + child: Column( + mainAxisAlignment: .center, + children: [ + Center( + child: SizedBox( + width: min( + constraints.maxWidth * 0.8, + constraints.maxHeight * 0.8, + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: _LatteArtImage( + image: imageAndIndex.$2!, + uiScale: 1, + isSelected: + selectedIndex == + imageAndIndex.$1, + onTap: () { + onSelectImage(imageAndIndex.$1); + }, + ), + ), + ), + ), + const SizedBox(height: 12), + ParagraphTextWidget( + imageAndIndex.$2!.description, + style: const TextStyle( + color: AppColors.white, + fontSize: 16, + ), + textAlign: .center, + maxLines: 3, + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), + ), + Expanded( + flex: 3, + child: ZipItem( + index: 0, + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: !shouldShowButtons + ? const SizedBox.shrink() + : Row( + mainAxisAlignment: .center, + children: [ + const Spacer(), + if (advanceToTweakImage != null) ...[ + Expanded( + flex: 3, + child: ResponsiveChevronButton( + onPressed: + (selectedIndex != null && + !isSubmitting) + ? advanceToAcceptOrder + : null, + color: Colors.transparent, + borderColor: AppColors.chevronYellow, + textColor: AppColors.chevronYellow, + style: .flat, + text: 'Accept', + ), + ), + Expanded( + flex: 3, + child: ResponsiveChevronButton( + onPressed: + (selectedIndex != null && + !isSubmitting) + ? advanceToTweakImage + : null, + style: .flat, + text: 'Tweak', + ), + ), + ], + + if (advanceToTweakImage == null) + Expanded( + flex: 5, + child: ResponsiveChevronButton( + onPressed: + (selectedIndex != null && + !isSubmitting) + ? advanceToAcceptOrder + : null, + style: .flat, + text: 'Accept', + ), + ), + const Spacer(), + ], + ), + ), + ), + ), + ], + ), + ), + LoadingProgress( + stillWaiting: realImagesCount == 0, + progressStartTime: imageGenerationStartTime, + padding: EdgeInsets.symmetric( + horizontal: info.width * 0.05, + vertical: info.height * 0.15, + ), + ), + ], + ); + }, + ); + } +} + +class _LatteArtField extends StatelessWidget { + const _LatteArtField({ + required this.advanceToAcceptOrder, + required this.advanceToTweakImage, + required this.imagesBatch, + required this.isSubmitting, + required this.layoutInfo, + required this.onSelectImage, + required this.selectedIndex, + required this.imageGenerationStartTime, + }); + + /// Callback to invoke when the user skips to the accept order step. + final VoidCallback? advanceToAcceptOrder; + + /// Callback to advance the wizard to the tweak image step. + final VoidCallback? advanceToTweakImage; + + /// The images to display. + final LatteImageBatch? imagesBatch; + + /// Whether the UI is submitting. + final bool isSubmitting; + + /// The layout information for the current screen. + final LayoutInformation layoutInfo; + + /// Callback for when the user selects a new image. + final void Function(int) onSelectImage; + + /// Selected image. + final int? selectedIndex; + + /// The time of start of image generation. + final DateTime? imageGenerationStartTime; + + static const double _defaultWidth = 1024; + static const double _defaultHeight = 768; + + @override + Widget build(BuildContext context) { + final images = imagesBatch?.images.toList() ?? _emptyImages; + final realImagesCount = images.whereNotNull().length; + final shouldShowButtons = realImagesCount > 0; + return Row( + children: [ + const Spacer(), + Expanded( + flex: 9, + child: LayoutProvider.builder( + builder: (context, info) => + _buildLatteArtImages(context, info, images), + ), + ), + Expanded( + flex: 2, + child: ZipItem( + index: 0, + child: !shouldShowButtons + ? const SizedBox.shrink() + : Column( + mainAxisAlignment: .center, + children: [ + if (advanceToTweakImage != null) ...[ + ResponsiveChevronButton( + onPressed: (selectedIndex != null && !isSubmitting) + ? advanceToTweakImage + : null, + style: .flat, + text: 'Tweak', + ), + const SizedBox(height: 16), + ResponsiveChevronButton( + onPressed: (selectedIndex != null && !isSubmitting) + ? advanceToAcceptOrder + : null, + color: Colors.transparent, + borderColor: AppColors.chevronYellow, + textColor: AppColors.chevronYellow, + style: .flat, + text: 'Accept', + ), + ], + if (advanceToTweakImage == null) + ResponsiveChevronButton( + onPressed: (selectedIndex != null && !isSubmitting) + ? advanceToAcceptOrder + : null, + style: .vertical, + scale: 0.6, + text: 'Accept', + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildLatteArtImages( + BuildContext context, + LayoutInformation info, + List images, + ) { + final realImagesCount = images.whereNotNull().length; + + final scale = info.unconstrainedScale( + const Size(_defaultWidth, _defaultHeight), + ); + + // Get the default latte art image positions + final latteArtPositions = _latteArtRowPositions(info, scale); + + // Now shift things around to accentuate the selected image + if (selectedIndex != null) { + _accentuateActiveImageInRow(latteArtPositions, scale); + } + + return Stack( + clipBehavior: .none, + fit: .expand, + children: [ + Positioned( + top: 0, + left: 0, + width: 0, + height: 0, + child: SizedBox.shrink( + // This is here for ease of testing (while also preventing + // a forced unmount of the whole widget tree). + key: ValueKey('step-choose-image-${imagesBatch?.id ?? 'none'}'), + ), + ), + ...latteArtPositions.indexed.map( + (positionAndIndex) { + final image = images[positionAndIndex.$1]; + final child = image == null + ? const EmptyLatteArt() + : ZipItem( + index: latteArtPositions.length - positionAndIndex.$1, + child: _LatteArtImage( + image: image, + uiScale: scale, + onTap: () { + onSelectImage(positionAndIndex.$1); + }, + isSelected: selectedIndex == positionAndIndex.$1, + ), + ); + return AnimatedPositioned( + duration: const Duration(seconds: 2), + curve: Curves.elasticOut, + top: positionAndIndex.$2.center.dy - positionAndIndex.$2.size / 2, + left: + positionAndIndex.$2.center.dx - positionAndIndex.$2.size / 2, + width: positionAndIndex.$2.size, + height: positionAndIndex.$2.size, + key: image != null + ? ValueKey(images[positionAndIndex.$1]!.imageUrl) + : null, + child: child, + ); + }, + ), + ...latteArtPositions.indexed.map( + (positionAndIndex) { + if (images[positionAndIndex.$1] == null) { + return null; + } + return Positioned( + top: + positionAndIndex.$2.center.dy + + positionAndIndex.$2.size / 2 + // bottom of the image + 12, // vertical margin + left: max( + positionAndIndex.$2.center.dx - positionAndIndex.$2.size / 2, + 0, + ), + width: positionAndIndex.$2.size, + height: 100 * scale, + key: ValueKey( + '${images[positionAndIndex.$1]}--' + '${images[positionAndIndex.$1]!.description}', + ), + child: ZipItem( + index: latteArtPositions.length - positionAndIndex.$1, + child: ParagraphTextWidget( + images[positionAndIndex.$1]!.description, + style: TextStyle( + color: AppColors.white, + fontSize: 28 * scale, + ), + textAlign: .center, + maxLines: positionAndIndex.$1 == selectedIndex ? 3 : 4, + ), + ), + ); + }, + ).whereNotNull(), + LoadingProgress( + stillWaiting: realImagesCount == 0, + progressStartTime: imageGenerationStartTime, + padding: EdgeInsets.symmetric( + horizontal: info.width * 0.05, + vertical: info.height * 0.05, + ), + ), + ], + ); + } + + List<_LatteArtPosition> _latteArtRowPositions( + LayoutInformation info, + double scale, + ) { + return [ + _LatteArtPosition( + center: Offset( + info.constraints.maxWidth * 0.12, + info.constraints.maxHeight * 0.35, + ), + size: 150 * scale, + ), + _LatteArtPosition( + center: Offset( + info.constraints.maxWidth * 0.35, + info.constraints.maxHeight * 0.5, + ), + size: 150 * scale, + ), + _LatteArtPosition( + center: Offset( + info.constraints.maxWidth * 0.58, + info.constraints.maxHeight * 0.4, + ), + size: 150 * scale, + ), + _LatteArtPosition( + center: Offset( + info.constraints.maxWidth * 0.81, + info.constraints.maxHeight * 0.45, + ), + size: 150 * scale, + ), + ]; + } + + /// Performs in-place modifications to [latteArtPositions] to make the + /// selected image larger and the other images shift away from the selected + /// image, all assuming the row layout. + void _accentuateActiveImageInRow( + List<_LatteArtPosition> latteArtPositions, + double scale, + ) { + latteArtPositions[selectedIndex!] = latteArtPositions[selectedIndex!].scale( + 1.5, + ); + + int delta = 1; + while (delta < latteArtPositions.length) { + final lowerIndexToModify = selectedIndex! - delta; + if (lowerIndexToModify >= 0) { + latteArtPositions[lowerIndexToModify] = + latteArtPositions[lowerIndexToModify].shiftAwayFrom( + latteArtPositions[selectedIndex!].center, + scale, + ); + } + + final upperIndexToModify = selectedIndex! + delta; + if (upperIndexToModify < latteArtPositions.length) { + latteArtPositions[upperIndexToModify] = + latteArtPositions[upperIndexToModify].shiftAwayFrom( + latteArtPositions[selectedIndex!].center, + scale, + ); + } + delta++; + } + } +} + +class _LatteArtImage extends StatelessWidget { + const _LatteArtImage({ + required this.image, + required this.isSelected, + required this.onTap, + required this.uiScale, + }); + + /// The image to display. + final LatteImage image; + + /// Whether the image is selected. + final bool isSelected; + + /// Callback to invoke when the user taps the image. + final VoidCallback onTap; + + /// The color of the halo. + static const Color haloColor = AppColors.googleBlue; + + /// The scale of the UI. + final double uiScale; + + @override + Widget build(BuildContext context) { + Widget child = LatteImageWidget(imageUrl: image.imageUrl); + + if (isSelected) { + child = Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: haloColor.withValues( + alpha: 0.8, + ), // Adjust opacity for desired intensity + blurRadius: 8 * uiScale, // How soft the edge of the glow is + spreadRadius: 8 * uiScale, // How much the glow expands outwards + // Default offset of Offset.zero centers the glow around the image + ), + ], + ), + child: child, + ); + } else { + child = GestureDetector(onTap: onTap, child: child); + } + + return child; + } +} + +class _LatteArtPosition { + const _LatteArtPosition({ + required this.center, + required this.size, + }); + + final Offset center; + final double size; + + _LatteArtPosition scale(double scale) { + return _LatteArtPosition( + center: center, + size: size * scale, + ); + } + + _LatteArtPosition shiftAwayFrom(Offset otherCenter, double uiScale) { + return _LatteArtPosition( + center: Offset( + center.dx + (center.dx - otherCenter.dx) * 0.05 * uiScale, + center.dy + (center.dy - otherCenter.dy) * 0.05 * uiScale, + ), + size: size, + ); + } +} + +extension _FilterableList on Iterable { + Iterable whereNotNull() { + return where((T? element) => element != null).cast(); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_confirmation.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_confirmation.dart new file mode 100644 index 0000000..ba637ae --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_confirmation.dart @@ -0,0 +1,112 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/kiosk/widgets/zip_item.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// The final screen in the kiosk wizard, where the user confirms their order. +class KioskConfirmation extends StatelessWidget { + /// Creates a [KioskConfirmation] widget. + const KioskConfirmation({ + required this.onNewOrder, + required this.orderNumber, + super.key, + }); + + /// Callback for when the user starts over. + final VoidCallback onNewOrder; + + /// The order number. + final int orderNumber; + + static const _breakpointWidth = 650; + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, info) { + final newline = info.width > _breakpointWidth ? '\n' : ' '; + + final scale = (min(info.width, _breakpointWidth) / _breakpointWidth) + .clamp(0.5, 1.0); + + final whiteTextTitle = TextStyle( + color: AppColors.white, + letterSpacing: 1.5, + fontWeight: FontWeight.w500, + fontSize: 44 * scale, + ); + final orderNumberStyle = whiteTextTitle.copyWith( + fontWeight: FontWeight.w600, + fontSize: 48 * scale, + ); + final whiteTextBody = whiteTextTitle.copyWith( + fontSize: 20 * scale, + ); + final whiteTextSmall = whiteTextTitle.copyWith( + fontSize: 14 * scale, + ); + + return Padding( + padding: EdgeInsets.symmetric(horizontal: info.width * 0.1), + child: Column( + children: [ + const Spacer(), + ZipItem( + index: 4, + child: Text( + 'Order #$orderNumber', + style: orderNumberStyle, + ), + ), + const SizedBox(height: 24), + ZipItem( + index: 3, + child: Text( + 'Pick up your order', + style: whiteTextTitle, + textAlign: .center, + ), + ), + const SizedBox(height: 24), + ZipItem( + index: 2, + child: Text( + 'Proceed to the line to pickup your order.$newline' + 'You can check your place in line on the order board.', + style: whiteTextBody, + textAlign: .center, + ), + ), + const SizedBox(height: 24), + ZipItem( + index: 1, + child: Text( + 'Want to know more?$newline' + 'Talk to someone by the pickup area about using Flutter or ' + 'Firebase.', + style: whiteTextSmall, + textAlign: .center, + ), + ), + const SizedBox(height: 36), + ZipItem( + index: 0, + child: GenLatteOutlinedButton.light( + label: 'Start a new order', + size: .large, + onPressed: onNewOrder, + ), + ), + const Spacer(), + ], + ), + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_drink_milk_sweetener.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_drink_milk_sweetener.dart new file mode 100644 index 0000000..83835f0 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_drink_milk_sweetener.dart @@ -0,0 +1,230 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:genlatte/src/screens/kiosk/widgets/zip_item.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; + +/// Milk / sweetener configuration screen. +class KioskDrinkMilkSweetener extends StatefulWidget { + /// Creates a new [KioskDrinkMilkSweetener]. + const KioskDrinkMilkSweetener({ + required this.advance, + required this.milkOptions, + required this.sweetenerOptions, + required this.selectedMilk, + required this.selectedSweetener, + required this.selectMilk, + required this.selectSweetener, + required this.username, + super.key, + }); + + /// Callback to advance to the next step of the wizard. + final VoidCallback? advance; + + /// The available milk options. + final List? milkOptions; + + /// The available sweetener options. + final List? sweetenerOptions; + + /// The selected milk. + final String? selectedMilk; + + /// The selected sweetener. + final String? selectedSweetener; + + /// Callback to select milk. + final void Function(String) selectMilk; + + /// Callback to select sweetener. + final void Function(String) selectSweetener; + + /// The name the user entered on the previous screen. + final String username; + + @override + State createState() => + _KioskDrinkMilkSweetenerState(); +} + +class _KioskDrinkMilkSweetenerState extends State { + int? _forcedIndex; + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, layoutInfo) { + return layoutInfo.aspectRatio > 1.6 + ? _buildRow(context) + : _buildColumn(context); + }, + ); + } + + Widget _buildRow(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + height: CardShape.tall.height, + child: Align( + child: Row( + mainAxisAlignment: .center, + children: [ + const Spacer(flex: 3), + Expanded( + flex: 4, + child: _MilkChoiceCard( + milkOptions: widget.milkOptions, + selectedMilk: widget.selectedMilk, + selectMilk: widget.selectMilk, + username: widget.username, + ), + ), + const Spacer(), + Expanded( + flex: 4, + child: _SweetenerChoiceCard( + sweetenerOptions: widget.sweetenerOptions, + selectedSweetener: widget.selectedSweetener, + selectSweetener: widget.selectSweetener, + ), + ), + Expanded( + flex: 3, + child: ZipItem( + index: 0, + child: ResponsiveChevronButton( + onPressed: widget.advance, + style: .vertical, + scale: 0.6, + text: 'Next', + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildColumn(BuildContext context) => Column( + children: [ + const Spacer(), + Expanded( + flex: 9, + child: SneakPeakCarousel( + previewPercentage: 0.2, + activeIndex: _forcedIndex, + onIndexChanged: (index) { + _forcedIndex = index; + }, + children: [ + _MilkChoiceCard( + milkOptions: widget.milkOptions, + selectedMilk: widget.selectedMilk, + selectMilk: widget.selectMilk, + username: widget.username, + ), + _SweetenerChoiceCard( + sweetenerOptions: widget.sweetenerOptions, + selectedSweetener: widget.selectedSweetener, + selectSweetener: widget.selectSweetener, + ), + ], + ), + ), + Expanded( + flex: 3, + child: ZipItem( + index: 0, + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: ResponsiveChevronButton( + onPressed: + widget.selectedMilk != null && + widget.selectedSweetener == null + ? () { + setState(() { + _forcedIndex = 1; + }); + } + : widget.advance, + style: .flat, + text: 'Next', + ), + ), + ), + ), + ], + ); +} + +class _MilkChoiceCard extends StatelessWidget { + const _MilkChoiceCard({ + required this.milkOptions, + required this.selectedMilk, + required this.selectMilk, + required this.username, + }); + + final List? milkOptions; + final String? selectedMilk; + final void Function(String) selectMilk; + final String username; + + @override + Widget build(BuildContext context) { + return ZipItem( + index: 2, + child: ConfigurationCard.buttons( + flavor: Flavor.wood, + onSelected: selectMilk, + question: MultipleChoiceQuestion( + id: 'milk', + body: + 'Hey $username, what kind of milk would you like in your ' + 'latte?', + acceptableAnswers: + milkOptions?.map((option) => option.name).toList() ?? const [], + selectedValue: selectedMilk, + ), + ), + ); + } +} + +class _SweetenerChoiceCard extends StatelessWidget { + const _SweetenerChoiceCard({ + required this.sweetenerOptions, + required this.selectedSweetener, + required this.selectSweetener, + }); + + final List? sweetenerOptions; + final String? selectedSweetener; + final void Function(String) selectSweetener; + + @override + Widget build(BuildContext context) { + return ZipItem( + index: 1, + child: ConfigurationCard.buttons( + flavor: .seaFoam, + onSelected: selectSweetener, + question: MultipleChoiceQuestion( + id: 'sweetener', + body: 'What kind of sweetener would you like?', + acceptableAnswers: + sweetenerOptions?.map((option) => option.name).toList() ?? + const [], + selectedValue: selectedSweetener, + ), + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_drink_order_name.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_drink_order_name.dart new file mode 100644 index 0000000..0767833 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_drink_order_name.dart @@ -0,0 +1,106 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:genlatte/src/screens/kiosk/widgets/zip_item.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart' show TextQuestion; + +/// First screen of the kiosk flow which asks for the user's name. +class KioskDrinkOrderName extends StatefulWidget { + /// Creates a new [KioskDrinkOrderName]. + const KioskDrinkOrderName({ + required this.advance, + required this.name, + super.key, + }); + + /// The user's name. + final String? name; + + /// Callback to advance to the next step of the wizard. + final void Function(String)? advance; + + @override + State createState() => _KioskDrinkOrderNameState(); +} + +class _KioskDrinkOrderNameState extends State { + late String _name; + + @override + void initState() { + super.initState(); + _name = widget.name ?? ''; + } + + void setName(String name) { + setState(() { + _name = name; + }); + } + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, layoutInfo) { + final Axis axis = layoutInfo.aspectRatio < 1.6 + ? .vertical + : .horizontal; + return Padding( + padding: axis == .vertical + ? const EdgeInsets.symmetric(horizontal: 32) + : EdgeInsets.zero, + child: Flex( + direction: axis, + mainAxisAlignment: .spaceEvenly, + children: [ + Spacer(flex: axis == .horizontal ? 3 : 1), + Expanded( + flex: 9, + child: ZipItem( + index: 1, + child: ResponsiveSizedBox( + aspectRatioClamp: (null, 745 / 400, null), + maxSize: const Size(745, 400), + child: ConfigurationCard.text( + flavor: Flavor.wood, + onSelected: setName, + question: TextQuestion( + id: 'name', + body: 'Can I get a first name for your drink order?', + answer: _name, + helpText: 'Your name', + ), + ), + ), + ), + ), + Expanded( + flex: 3, + child: ZipItem( + index: 0, + child: Padding( + padding: axis == .vertical + ? const EdgeInsets.only(top: 16) + : EdgeInsets.zero, + child: ResponsiveChevronButton( + onPressed: widget.advance != null && _name.isNotEmpty + ? () { + widget.advance!.call(_name); + } + : null, + scale: axis == .horizontal ? 0.6 : null, + text: 'Next', + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_happy_place.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_happy_place.dart new file mode 100644 index 0000000..7874d63 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_happy_place.dart @@ -0,0 +1,410 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/services.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/kiosk/widgets/zip_item.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:text_responsive/text_responsive.dart'; + +/// {@template KioskHappyPlace} +/// The screen where the user is asked for their happy place. +/// {@endtemplate} +class KioskHappyPlace extends StatefulWidget { + /// {@macro KioskHappyPlace} + const KioskHappyPlace({ + required this.happyPlace, + required this.submitHappyPlace, + super.key, + }); + + /// The user's happy place. + final String? happyPlace; + + /// Callback to submit the user's happy place. + final void Function(String)? submitHappyPlace; + + @override + State createState() => _KioskHappyPlaceState(); +} + +class _KioskHappyPlaceState extends State { + final TextEditingController _controller = TextEditingController(); + + // Used to track when the happy place changes from falsey to truthy to + // activate the button + String? _happyPlace; + + @override + void initState() { + super.initState(); + _controller.text = widget.happyPlace ?? ''; + _controller.addListener(_updateHappyPlaceLocally); + _happyPlace = widget.happyPlace; + } + + @override + void didUpdateWidget(covariant KioskHappyPlace oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.happyPlace == null && oldWidget.happyPlace != null) { + setState(() { + _controller.clear(); + _happyPlace = null; + }); + } + } + + DateTime? _lastToastTime; + + void _showLimitReachedToast() { + final now = DateTime.now(); + if (_lastToastTime == null || + now.difference(_lastToastTime!) > const Duration(seconds: 3)) { + _lastToastTime = now; + showToast( + context: context, + builder: (context, overlay) { + return SurfaceCard( + child: Basic( + title: const Text( + 'Character limit reached', + style: TextStyle(color: AppColors.googleIntroRed), + ).large, + subtitle: const Text( + 'The happy place must be 50 characters or less', + style: TextStyle(color: AppColors.black), + ).base, + trailing: OutlineButton( + onPressed: overlay.close, + child: const Text( + 'OK', + style: TextStyle(color: AppColors.googleIntroRed), + ), + ), + ), + ); + }, + location: ToastLocation.bottomCenter, + showDuration: const Duration(seconds: 3), + ); + } + } + + void _updateHappyPlaceLocally() { + final String? oldHappyPlace = _happyPlace; + _happyPlace = _controller.text; + if (oldHappyPlace == null || + oldHappyPlace.isEmpty && _happyPlace!.isNotEmpty) { + // Trigger an update to activate the button. + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, layoutInfo) { + final Axis axis = layoutInfo.aspectRatio < 1.6 + ? .vertical + : .horizontal; + return Padding( + padding: axis == .vertical + ? const EdgeInsets.symmetric(horizontal: 24) + : EdgeInsets.zero, + child: Flex( + direction: axis, + children: [ + if (axis == .horizontal) const Spacer(flex: 3), + if (axis == .vertical) const Spacer(), + Expanded( + flex: 9, + child: ZipItem( + index: 1, + child: ResponsiveSizedBox( + aspectRatioClamp: ( + CardShape.tall.aspectRatio, + null, + CardShape.expanded.aspectRatio, + ), + maxSize: CardShape.expanded.size * 1.2, + child: _HappyPlaceConfigurationCard( + controller: _controller, + onLimitReached: _showLimitReachedToast, + ), + ), + ), + ), + Expanded( + flex: 3, + child: ZipItem( + index: 0, + child: Padding( + padding: axis == .vertical + ? const EdgeInsets.only(top: 16) + : EdgeInsets.zero, + child: ResponsiveChevronButton( + onPressed: + widget.submitHappyPlace != null && + _happyPlace != null && + _happyPlace!.isNotEmpty + ? () => widget.submitHappyPlace!(_happyPlace!) + : null, + scale: axis == .horizontal ? 0.6 : null, + text: 'Next', + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} + +class _HappyPlaceConfigurationCard extends StatelessWidget { + const _HappyPlaceConfigurationCard({ + required this.controller, + required this.onLimitReached, + }); + + final TextEditingController controller; + final VoidCallback onLimitReached; + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, layout) { + // 1.3 chosen by eyeballing when the quadrants start to feel crowded + return layout.aspectRatio < 1.3 + ? _buildColumn(context, layout) + : _buildQuadrants(context, layout); + }, + ); + } + + Widget _buildColumn(BuildContext context, LayoutInformation layout) { + final double edgeInsets = + (layout.constrainingDimension * 0.1) // + .clamp(16, 64); + final stackConstraints = BoxConstraints.tight( + Size( + layout.width - (edgeInsets * 2), + layout.height - (edgeInsets * 2), + ), + ); + + final double floor = layout.constrainingDimension > 250 ? 1 : 0.5; + final double inputScalar = max(floor, layout.constrainingDimension / 400); + final scaledTextStyle = Theme.of(context).typography.large.copyWith( + fontSize: Theme.of(context).typography.large.fontSize! * inputScalar, + ); + return Container( + decoration: BoxDecoration( + color: Flavor.weirdGreen.backgroundColor, + borderRadius: BorderRadius.circular(16), + ), + padding: EdgeInsets.all(edgeInsets), + child: Stack( + children: [ + Positioned( + left: 0, + right: 0, + top: 0, + height: stackConstraints.maxHeight * 0.30, + child: ParagraphTextWidget( + 'Ok, now tell me about your happy place', + style: Theme.of(context).typography.h1, + maxLines: 3, + ), + ), + Positioned( + left: 0, + right: 0, + height: stackConstraints.maxHeight * 0.65, + bottom: 0, + child: ShaderMask( + shaderCallback: (rect) { + return const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black, Colors.transparent], + stops: [0.6, 1], + ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); + }, + blendMode: BlendMode.dstIn, + child: Image.asset('assets/flutter-latte.png'), + ), + ), + Positioned( + left: 0, + bottom: 0, + width: stackConstraints.maxWidth, + height: stackConstraints.maxHeight * 0.4, + child: Column( + crossAxisAlignment: .start, + mainAxisAlignment: .end, + children: [ + TextField( + autocorrect: false, + controller: controller, + decoration: BoxDecoration( + border: const Border(), // defaults to BorderStyle.none + borderRadius: BorderRadius.all( + Radius.circular(8 * inputScalar), + ), + color: Theme.of(context).colorScheme.input, + ), + inputFormatters: [ + CharacterLimitFormatter(50, onLimitReached), + ], + maxLength: 50, + placeholder: Text( + 'Imagine your happy place', + overflow: .ellipsis, + style: scaledTextStyle, + ), + style: scaledTextStyle, + ), + const SizedBox(height: 12), + if (layout.constrainingDimension > 250) + const ParagraphTextWidget('This will inspire your latte!'), + ], + ), + ), + ], + ), + ); + } + + Widget _buildQuadrants(BuildContext context, LayoutInformation layout) { + final double edgeInsets = + (layout.constrainingDimension * 0.12) // + .clamp(16, 64); + final stackConstraints = BoxConstraints.tight( + Size( + layout.width - (edgeInsets * 2), + layout.height - (edgeInsets * 2), + ), + ); + + final double floor = layout.constrainingDimension > 250 ? 1 : 0.5; + final double inputScalar = max(floor, layout.constrainingDimension / 400); + final textStyle = Theme.of(context).typography.large; + final scaledTextStyle = textStyle.copyWith( + fontSize: textStyle.fontSize! * inputScalar, + ); + final scaledSmallTextStyle = Theme.of(context).typography.small.copyWith( + fontSize: Theme.of(context).typography.small.fontSize! * inputScalar, + ); + return Container( + decoration: BoxDecoration( + color: Flavor.weirdGreen.backgroundColor, + borderRadius: BorderRadius.circular(16), + ), + padding: EdgeInsets.all(edgeInsets), + child: Stack( + children: [ + Positioned( + right: 0, + top: 0, + bottom: 0, + width: stackConstraints.maxWidth * 0.4, + child: Image.asset('assets/flutter-latte.png'), + ), + Positioned( + left: 0, + top: 0, + width: stackConstraints.maxWidth * 0.55, + height: stackConstraints.maxHeight * 0.55, + child: ParagraphTextWidget( + 'Ok, now tell me about your happy place', + style: Theme.of(context).typography.h1, + maxLines: 3, + ), + ), + Positioned( + left: 0, + bottom: 0, + width: stackConstraints.maxWidth * 0.6, + height: stackConstraints.maxHeight * 0.4, + child: Column( + crossAxisAlignment: .start, + mainAxisAlignment: .end, + children: [ + TextField( + autocorrect: false, + controller: controller, + decoration: BoxDecoration( + border: const Border(), // defaults to BorderStyle.none + borderRadius: BorderRadius.all( + Radius.circular(8 * inputScalar), + ), + color: Theme.of(context).colorScheme.input, + ), + inputFormatters: [ + CharacterLimitFormatter(50, onLimitReached), + ], + maxLength: 50, + placeholder: Text( + 'Imagine your happy place', + style: scaledTextStyle, + ), + style: scaledTextStyle, + ), + const SizedBox(height: 12), + if (layout.constrainingDimension > 250) + Text( + 'This will inspire your latte!', + style: scaledSmallTextStyle, + ), + ], + ), + ), + ], + ), + ); + } +} + +/// A [TextInputFormatter] that limits the number of characters in a +/// [TextField]. +class CharacterLimitFormatter extends TextInputFormatter { + /// Creates a new [CharacterLimitFormatter]. + CharacterLimitFormatter(this.limit, this.onLimitReached); + + /// The maximum number of characters allowed. + final int limit; + + /// Callback to be invoked when the character limit is reached. + final VoidCallback onLimitReached; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (newValue.text.length > limit) { + onLimitReached(); + if (oldValue.text.length >= limit) { + return oldValue; + } + return TextEditingValue( + text: newValue.text.substring(0, limit), + selection: TextSelection.collapsed(offset: limit), + ); + } + return newValue; + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_intro.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_intro.dart new file mode 100644 index 0000000..82b9376 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_intro.dart @@ -0,0 +1,296 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/kiosk/widgets/zip_item.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:text_responsive/text_responsive.dart'; + +const _whiteThinStyle = TextStyle( + color: AppColors.white, + fontWeight: FontWeight.normal, +); + +const _whiteBoldStyle = TextStyle( + color: AppColors.white, + fontWeight: FontWeight.bold, +); + +/// First screen of the kiosk flow which asks for the user's name. +class KioskIntro extends StatelessWidget { + /// Creates a new [KioskIntro]. + const KioskIntro({required this.advance, super.key}); + + /// Callback to advance to the next step. + final VoidCallback advance; + + @override + Widget build(BuildContext context) { + final layoutInfo = context.watch(); + final intraCardSpacer = layoutInfo.orientation.isLandscape + ? const SizedBox(width: 16) + : const SizedBox(height: 16); + final startButtonSpacer = layoutInfo.orientation.isLandscape + ? const SizedBox(width: 24) + : const SizedBox(height: 24); + return Padding( + padding: layoutInfo.orientation.axis == .vertical + ? const EdgeInsets.symmetric(horizontal: 32) + : EdgeInsets.zero, + child: Flex( + direction: layoutInfo.orientation.axis, + children: [ + const Spacer(), + Expanded( + flex: 10, + child: Flex( + direction: layoutInfo.orientation.axis, + children: [ + const Expanded( + flex: 3, + child: ZipItem( + index: 3, + child: _IntroCard( + color: AppColors.googleIntroRed, + stepIndex: 1, + cta: 'Customize\nyour latte', + ), + ), + ), + intraCardSpacer, + + const Expanded( + flex: 3, + child: ZipItem( + index: 2, + child: _IntroCard( + color: AppColors.googleIntroBlue, + stepIndex: 2, + cta: 'Create your\nlatte art', + ), + ), + ), + intraCardSpacer, + const Expanded( + flex: 3, + child: ZipItem( + index: 1, + child: _IntroCard( + color: AppColors.googleIntroGreen, + stepIndex: 3, + cta: 'Pick up\nyour drink', + helpText: 'AT THE BARISTA STATION', + ), + ), + ), + startButtonSpacer, + ], + ), + ), + Expanded( + flex: 2, + child: ZipItem( + index: 0, + child: ResponsiveChevronButton( + onPressed: advance, + text: 'Start', + ), + ), + ), + const Spacer(), + ], + ), + ); + } +} + +class _IntroCard extends StatelessWidget { + const _IntroCard({ + required this.color, + required this.stepIndex, + required this.cta, + this.helpText, + }); + + final Color color; + + /// Number to show in a circle above the text. + final int stepIndex; + + /// Main text. + final String cta; + + /// Optional text below the [cta]. + final String? helpText; + + /// Standard width when the UI is in landscape mode (and the card itself is + /// vertical); + static const _defaultLandscapeWidth = 200; + + /// Standard height when the UI is in portrait mode (and the card itself is + /// horizontal); + static const _defaultPortraitHeight = 110; + + /// Minimum available width below which we must further shrink font sizes + /// while the UI is in portrait mode (and the card is using a Column) + static const _minPortraitWidth = 350; + + static const _minLandscapeAspectRatio = 0.65; + static const _maxLandscapeAspectRatio = 0.933; + + static const _minPortraitAspectRatio = 1.3; + static const _maxPortraitAspectRatio = 4.0; + + @override + Widget build(BuildContext context) { + final layoutInfo = context.watch(); + return ResponsiveSizedBox.builder( + aspectRatioClamp: + (layoutInfo.orientation.isLandscape + ? ( + _minLandscapeAspectRatio, + null, + _maxLandscapeAspectRatio, + ) + : ( + _minPortraitAspectRatio, + null, + _maxPortraitAspectRatio, + )) + as AcceptableAspectRatios, + builder: (context, size) { + final constraints = BoxConstraints( + maxWidth: size.width, + maxHeight: size.height, + ); + return Container( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(20), + ), + child: layoutInfo.orientation == .landscape + ? _buildColumn(context, constraints) + : _buildRow(context, constraints), + ); + }, + ); + } + + Widget _buildRow(BuildContext context, BoxConstraints constraints) { + double fontScalar = constraints.maxHeight / _defaultPortraitHeight; + if (constraints.maxWidth < _minPortraitWidth) { + fontScalar = fontScalar.clamp( + constraints.maxWidth / _minPortraitWidth, + 1, + ); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + child: Row( + mainAxisAlignment: .spaceBetween, + children: [ + _StepCircle(stepIndex: stepIndex, fontScalar: fontScalar), + const Spacer(), + Column( + mainAxisAlignment: .center, + children: [ + Text( + cta, + textAlign: TextAlign.center, + style: _whiteThinStyle.copyWith( + fontSize: 24 * fontScalar, + ), + ), + if (helpText != null && helpText!.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + helpText!, + textAlign: TextAlign.center, + style: _whiteThinStyle.copyWith( + fontSize: 11 * fontScalar, + ), + ), + ], + ], + ), + const Spacer(), + ], + ), + ); + } + + Widget _buildColumn( + BuildContext context, + BoxConstraints constraints, + ) { + double fontScalar = constraints.maxWidth / _defaultLandscapeWidth; + final aspectRatio = constraints.maxWidth / constraints.maxHeight; + + // If the aspect ratio is too flat (too landscape), then a really wide width + // can calculate text sizes that are too large for the available height. + if (aspectRatio > _maxLandscapeAspectRatio) { + fontScalar = fontScalar * (_maxLandscapeAspectRatio / aspectRatio); + } + + double topPaddingScalar = 1; + if (aspectRatio < _minLandscapeAspectRatio) { + topPaddingScalar = _minLandscapeAspectRatio / aspectRatio; + } + + return Column( + children: [ + SizedBox(height: constraints.maxHeight * 0.15 * topPaddingScalar), + _StepCircle(stepIndex: stepIndex, fontScalar: fontScalar), + Padding( + padding: const EdgeInsets.only(top: 16), + child: ParagraphTextWidget( + cta, + textAlign: TextAlign.center, + style: _whiteThinStyle.copyWith( + fontSize: 21 * fontScalar, + ), + ), + ), + if (helpText != null && helpText!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 16), + child: ParagraphTextWidget( + helpText!, + textAlign: TextAlign.center, + style: _whiteThinStyle.copyWith( + fontSize: 10 * fontScalar, + ), + ), + ), + const SizedBox(height: 32), + ], + ); + } +} + +class _StepCircle extends StatelessWidget { + const _StepCircle({required this.stepIndex, required this.fontScalar}); + + final int stepIndex; + final double fontScalar; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: AppColors.white), + shape: BoxShape.circle, + ), + padding: EdgeInsets.all(16 * fontScalar), + child: Text( + stepIndex.toString(), + style: _whiteBoldStyle.copyWith( + fontSize: 18 * fontScalar, + ), + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_step.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_step.dart new file mode 100644 index 0000000..6369a59 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_step.dart @@ -0,0 +1,52 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Generic layout wrapper for Kiosk steps. +class KioskStep extends StatelessWidget { + /// Instantiates a new [KioskStep]. + const KioskStep({ + required this.child, + this.header, + super.key, + }); + + /// Optional header widget to exist below the segmented progress indicator. + final Widget? header; + + /// The content of the step. + final Widget child; + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, info) { + final verticalPadding = + info.height * (info.orientation.isLandscape ? 0.07 : 0.035); + return Stack( + children: [ + if (header != null) ...[ + Positioned( + left: 0, + right: 0, + top: 0, + height: info.height * 0.2, + child: header!, + ), + ], + Positioned( + left: 0, + right: 0, + top: verticalPadding, + bottom: verticalPadding, + child: child, + ), + ], + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_submit_order.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_submit_order.dart new file mode 100644 index 0000000..cc1d996 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_submit_order.dart @@ -0,0 +1,304 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' show min; + +import 'package:flutter_svg/svg.dart'; +import 'package:genlatte/src/screens/kiosk/widgets/zip_item.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// The final step in the kiosk wizard, where the user accepts their order. +class KioskSubmitOrder extends StatelessWidget { + /// Creates a [KioskSubmitOrder] widget. + const KioskSubmitOrder({ + required this.advance, + required this.metadata, + required this.order, + required this.returnToLatteConfig, + super.key, + }); + + /// Callback for when the user accepts the order. + /// + /// If the value is null then the order is probably being submitted as we + /// speak. + final VoidCallback? advance; + + /// The order to display. + final LatteOrder order; + + /// The order's server-owned metadata. + final LatteOrderMetadata metadata; + + /// Press handler for the "Edit drink" button. + final VoidCallback returnToLatteConfig; + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, info) { + return info.aspectRatio < 1.6 + ? _buildColumn(context, info) + : _buildRow(context, info); + }, + ); + } + + Widget _buildColumn(BuildContext context, LayoutInformation info) { + final imageDiameter = min(info.width * 0.5, info.height * 0.3); + final verticalScalar = info.height / 500; + final h3ScaledText = _whiteText.copyWith( + fontSize: 24 * verticalScalar, + ); + final pTagScaledText = _whiteText.copyWith( + fontSize: 14 * verticalScalar, + ); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + children: [ + if (metadata.imageUrl != null) + ZipItem( + index: 2, + child: LatteImageWidget( + imageUrl: metadata.imageUrl!, + dimension: imageDiameter, + ), + ), + if (metadata.imageUrl == null) + SizedBox( + height: imageDiameter, + width: imageDiameter, + child: const CircularProgressIndicator(), + ), + SizedBox(height: 12 * verticalScalar), + ZipItem( + index: 2, + child: Text( + 'Order recap for ${order.name}', + textAlign: .center, + style: h3ScaledText, + ), + ), + SizedBox(height: 12 * verticalScalar), + ZipItem( + index: 2, + child: Wrap( + alignment: .center, + spacing: 24, + runSpacing: 12, + children: [ + Row( + mainAxisSize: .min, + children: [ + const GreenCheckMark(), + SizedBox(width: 8 * verticalScalar), + Text('Latte', style: pTagScaledText), + ], + ), + Row( + mainAxisSize: .min, + children: [ + const GreenCheckMark(), + SizedBox(width: 8 * verticalScalar), + Text( + '${order.milk!} milk', + style: pTagScaledText, + ), + ], + ), + Row( + mainAxisSize: .min, + children: [ + const GreenCheckMark(), + SizedBox(width: 8 * verticalScalar), + Text( + order.sweetener!.toLowerCase() != 'none' + ? order.sweetener! + : 'No sweetener', + style: pTagScaledText, + ), + ], + ), + ], + ), + ), + SizedBox(height: 12 * verticalScalar), + ZipItem( + index: 2, + child: GenLatteOutlinedButton.light( + onPressed: returnToLatteConfig, + label: 'Edit drink', + ), + ), + SizedBox(height: 24 * verticalScalar), + ZipItem( + index: 1, + child: Text( + 'Your happy place latte art:', + textAlign: .center, + style: h3ScaledText, + ), + ), + SizedBox(height: 8 * verticalScalar), + ZipItem( + index: 1, + child: Text( + '“${order.happyPlace!.trim()}”', + textAlign: .center, + style: h3ScaledText.copyWith(fontWeight: .bold), + ), + ), + const Spacer(), + ZipItem( + index: 0, + child: ResponsiveChevronButton( + onPressed: advance, + style: .flat, + scale: 0.6 * verticalScalar, + text: 'Submit', + ), + ), + const Spacer(), + ], + ), + ); + } + + Widget _buildRow(BuildContext context, LayoutInformation info) { + final topPadding = info.height * 0.15; + final horizontalPadding = info.width * 0.1; + final availableHorizontalSpace = info.width - (horizontalPadding * 2); + final availableVerticalSpace = info.height - (topPadding * 2); + + final statusColumnWidth = availableHorizontalSpace * 0.3; + final imageColumnWidth = availableHorizontalSpace * 0.4; + final submitButtonWidth = availableHorizontalSpace * 0.3; + + const imagePadding = EdgeInsets.fromLTRB(32, 0, 32, 32); + final imageDiameter = min( + imageColumnWidth - (imagePadding.left + imagePadding.right), + availableVerticalSpace - (imagePadding.top + imagePadding.bottom), + ); + + final milkConfigText = _whiteText.copyWith( + fontSize: 36 * statusColumnWidth / 300, + ); + + /// Number of pixels down from the top of the row until the center of the + /// image + final centerOfImageY = imagePadding.top + imageDiameter / 2; + + return Stack( + children: [ + Positioned( + top: topPadding, + left: horizontalPadding, + width: statusColumnWidth, + height: availableVerticalSpace, + child: ZipItem( + index: 2, + child: Column( + crossAxisAlignment: .start, + children: [ + const Text('Order recap for', style: _whiteText), + Text('${order.name}:', style: milkConfigText), + const Spacer(), + Row( + children: [ + const GreenCheckMark(), + const SizedBox(width: 12), + Text('Latte', style: milkConfigText), + ], + ), + Row( + children: [ + const GreenCheckMark(), + const SizedBox(width: 12), + Text('${order.milk} milk', style: milkConfigText), + ], + ), + Row( + children: [ + const GreenCheckMark(), + const SizedBox(width: 12), + if (order.sweetener!.toLowerCase() == 'none') + Text('No sweetener', style: milkConfigText), + if (order.sweetener!.toLowerCase() != 'none') + Text('${order.sweetener}', style: milkConfigText), + ], + ), + const Spacer(), + GenLatteOutlinedButton.light( + onPressed: returnToLatteConfig, + label: 'Edit drink', + ), + const Spacer(flex: 3), + ], + ), + ), + ), + Positioned( + top: topPadding, + left: horizontalPadding + statusColumnWidth, + width: imageColumnWidth, + height: availableVerticalSpace, + child: ZipItem( + index: 1, + child: Column( + children: [ + Center( + child: LatteImageWidget( + imageUrl: metadata.imageUrl!, + dimension: imageDiameter, + ), + ), + SizedBox(height: imagePadding.bottom), + const Text('Your happy place latte art:', style: _whiteText), + const SizedBox(height: 12), + Text( + '${order.happyPlace}', + textAlign: .center, + style: _whiteText, + ).h3, + ], + ), + ), + ), + Positioned( + top: topPadding, + right: horizontalPadding, + width: submitButtonWidth, + height: centerOfImageY * 2, + child: ZipItem( + index: 0, + child: ResponsiveChevronButton( + onPressed: advance, + style: .flat, + scale: submitButtonWidth / 300, + text: 'Submit', + ), + ), + ), + ], + ); + } +} + +/// A green check mark icon. +class GreenCheckMark extends StatelessWidget { + /// Creates a [GreenCheckMark] widget. + const GreenCheckMark({super.key}); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset('assets/green-check.svg'); + } +} + +const _whiteText = TextStyle( + color: Colors.white, +); diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_tweak_image.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_tweak_image.dart new file mode 100644 index 0000000..3a04f15 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/kiosk_tweak_image.dart @@ -0,0 +1,308 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/screens/kiosk/widgets/zip_item.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +// import 'package:web_browser_detect/web_browser_detect.dart'; + +/// Answer questions to tweak a selected image. +class KioskTweakImage extends StatefulWidget { + /// Creates a new [KioskTweakImage]. + KioskTweakImage({ + required this.advance, + required this.questions, + required this.onAnswer, + super.key, + }) : assert( + questions.isEmpty || (questions.length >= 2 && questions.length <= 4), + 'Must provide 0 or 2-4 questions', + ); + + /// Callback to advance to the next step of the wizard. + final VoidCallback? advance; + + /// Callback when a question is answered. + final void Function(Question, Object?) onAnswer; + + /// The questions to answer. + final List questions; + + @override + State createState() => _KioskTweakImageState(); +} + +class _KioskTweakImageState extends State { + // static const double _minHeightForFieldView = 400; + + late final TextEditingController _textController; + late final FocusNode _focusNode; + late final Map _cardKeys; + + @override + void initState() { + super.initState(); + final textQuestion = widget.questions.whereType().firstOrNull; + _textController = TextEditingController(text: textQuestion?.answer); + _focusNode = FocusNode(); + _cardKeys = { + for (final q in widget.questions) q.id: GlobalKey(debugLabel: q.id), + }; + } + + @override + void dispose() { + _textController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + Widget _buildQuestionCard(int index, Question question) { + return KeyedSubtree( + key: _cardKeys[question.id], + child: ResponsiveSizedBox( + aspectRatioClamp: (0.5, null, 3), + child: ConfigurationCard.forQuestion( + question, + flavor: Flavor.forIndex(index), + onAnswer: (answer) => widget.onAnswer(question, answer), + textEditingController: question is TextQuestion + ? _textController + : null, + focusNode: question is TextQuestion ? _focusNode : null, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + if (widget.questions.isEmpty) { + return const Center(child: CircularProgressIndicator(size: 48)); + } + + // bool forceCarousel = false; + // if (kIsWeb) { + // final browser = Browser(); + // if (browser.browserAgent == .chrome) { + // forceCarousel = true; + // } + // } + + return LayoutProvider.builder( + builder: _buildCarousel, + // info.aspectRatio < 1.6 || + // info.height < _minHeightForFieldView || + // forceCarousel + // ? _buildCarousel(context, info) + // : _buildField(context, info), + ); + } + + Widget _buildField(BuildContext context, LayoutInformation info) { + return Row( + children: [ + const Spacer(flex: 3), + Expanded( + flex: 9, + child: LayoutProvider.builder( + builder: (context, layoutInfo) { + return Stack( + children: switch (widget.questions.length) { + 2 => _buildTwoQuestions(context, layoutInfo), + 3 => _buildThreeQuestions(context, layoutInfo), + 4 => _buildFourQuestions(context, layoutInfo), + _ => throw UnimplementedError(), + }, + ); + }, + ), + ), + Expanded( + flex: 3, + child: ZipItem( + index: 0, + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: ResponsiveChevronButton( + onPressed: widget.advance, + scale: 0.6, + text: 'Next', + ), + ), + ), + ), + ], + ); + } + + List _buildTwoQuestions( + BuildContext context, + LayoutInformation info, + ) { + return [ + Positioned( + top: 0, + left: 0, + width: info.size.width * 0.46, + height: info.size.height, + key: ValueKey(widget.questions[0].id), + child: ZipItem( + index: 2, + child: _buildQuestionCard(0, widget.questions[0]), + ), + ), + Positioned( + top: 0, + right: 0, + width: info.size.width * 0.46, + height: info.size.height, + key: ValueKey(widget.questions[1].id), + child: ZipItem( + index: 1, + child: _buildQuestionCard(1, widget.questions[1]), + ), + ), + ]; + } + + List _buildThreeQuestions( + BuildContext context, + LayoutInformation layoutInfo, + ) { + return [ + Positioned( + top: 0, + left: 0, + width: layoutInfo.size.width * 0.46, + height: layoutInfo.size.height, + key: ValueKey(widget.questions[0].id), + child: ZipItem( + index: 2, + child: _buildQuestionCard(0, widget.questions[0]), + ), + ), + Positioned( + top: 0, + right: 0, + width: layoutInfo.size.width * 0.46, + height: layoutInfo.size.height * 0.46, + key: ValueKey(widget.questions[1].id), + child: ZipItem( + index: 1, + child: _buildQuestionCard(1, widget.questions[1]), + ), + ), + Positioned( + bottom: 0, + right: 0, + width: layoutInfo.size.width * 0.46, + height: layoutInfo.size.height * 0.46, + key: ValueKey(widget.questions[2].id), + child: ZipItem( + index: 1, + child: _buildQuestionCard(2, widget.questions[2]), + ), + ), + ]; + } + + List _buildFourQuestions( + BuildContext context, + LayoutInformation layoutInfo, + ) { + return [ + Positioned( + top: 0, + left: 0, + width: layoutInfo.size.width * 0.46, + height: layoutInfo.size.height * 0.46, + key: ValueKey(widget.questions[0].id), + child: ZipItem( + index: 2, + child: _buildQuestionCard(0, widget.questions[0]), + ), + ), + Positioned( + bottom: 0, + left: 0, + width: layoutInfo.size.width * 0.46, + height: layoutInfo.size.height * 0.46, + key: ValueKey(widget.questions[1].id), + child: ZipItem( + index: 2, + child: _buildQuestionCard(1, widget.questions[1]), + ), + ), + Positioned( + top: 0, + right: 0, + width: layoutInfo.size.width * 0.46, + height: layoutInfo.size.height * 0.46, + key: ValueKey(widget.questions[2].id), + child: ZipItem( + index: 1, + child: _buildQuestionCard(2, widget.questions[2]), + ), + ), + Positioned( + bottom: 0, + right: 0, + width: layoutInfo.size.width * 0.46, + height: layoutInfo.size.height * 0.46, + key: ValueKey(widget.questions[3].id), + child: ZipItem( + index: 1, + child: _buildQuestionCard(3, widget.questions[3]), + ), + ), + ]; + } + + Widget _buildCarousel(BuildContext context, LayoutInformation info) { + int? activeIndex; + if (_focusNode.hasFocus) { + final textQuestionIndex = widget.questions.indexWhere( + (q) => q is TextQuestion, + ); + if (textQuestionIndex != -1) { + activeIndex = textQuestionIndex; + } + } + + return Column( + children: [ + const Spacer(), + Expanded( + flex: 9, + child: SneakPeakCarousel( + activeIndex: activeIndex, + children: widget.questions.indexed + .map( + (q) => ZipItem( + index: q.$1, + child: _buildQuestionCard(q.$1, q.$2), + ), + ) + .toList(), + ), + ), + Expanded( + flex: 3, + child: ZipItem( + index: 0, + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: ResponsiveChevronButton( + onPressed: widget.advance, + text: 'Next', + ), + ), + ), + ), + ], + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/steps.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/steps.dart new file mode 100644 index 0000000..30531b1 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/home/steps/steps.dart @@ -0,0 +1,13 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'kiosk_choose_an_image.dart'; +export 'kiosk_confirmation.dart'; +export 'kiosk_drink_milk_sweetener.dart'; +export 'kiosk_drink_order_name.dart'; +export 'kiosk_happy_place.dart'; +export 'kiosk_intro.dart'; +export 'kiosk_step.dart'; +export 'kiosk_submit_order.dart'; +export 'kiosk_tweak_image.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/kiosk.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/kiosk.dart new file mode 100644 index 0000000..dafafd1 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/kiosk.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'home/kiosk_home.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/CONTEXT.md new file mode 100644 index 0000000..cc2b782 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/CONTEXT.md @@ -0,0 +1,27 @@ +# Kiosk Widgets + +**Purpose:** +A library of modular, common widgets used to flesh out the user interface and structural behavior of the Kiosk screens. Features page transition tools and progress indicators. + +**Detailed File Overviews:** + +- `empty_latte_art.dart`: + - **Description**: Handles drawing an outline cup/mug, utilized before user-generated imagery is available. + +- `segmented_progress.dart`: + - **Description**: A horizontal progress bar indicating completion steps. + - **Core Logic**: Renders a row of pill shapes, animating the width and color of active/inactive segments to telegraph the user's progress through the Kiosk wizard. + +- `zip_page_view.dart`: + - **Description**: A custom, complex page navigator (`ZipPageView`, `ZipPage`, `ZipPageState`). + - **Core Logic**: Overrides standard `PageView` physics and handles "zipping" items in and out sequentially across wizards. Calculates precise staggering delay offsets (`_staggerDelay`) based on a descending registration of sub-item indices, causing left-most or right-most elements to cascade elegantly via `CurvedAnimation`. + - **Usage/Exports**: This wraps the entirety of the `KioskHomeScreen` wizard workflow. + +- `zip_item.dart`: + - **Description**: A wrapper for individual elements displayed on a `ZipPage`, causing them to bind to the page's transition animations seamlessly. + +- `widgets.dart`: + - **Description**: Barrel export file. + +**Dependencies/Relationships:** +- Utilized entirely by `kiosk/home` and its `steps` subdirectory to render polished onboarding and ordering flows. diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/empty_latte_art.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/empty_latte_art.dart new file mode 100644 index 0000000..7757a65 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/empty_latte_art.dart @@ -0,0 +1,224 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +// Class to hold unique data for each swirling arc per instance +class _ArcConfig { + _ArcConfig({ + required this.phaseOffset, + required this.baseRadiusFactor, + required this.speedFactor, + required this.opacityFactor, + required this.maxSweep, + }); + + // Factory constructor for generating a random config for one arc + factory _ArcConfig.random() { + final rand = math.Random(); + return _ArcConfig( + phaseOffset: rand.nextDouble() * math.pi * 2, // Random starting phase + baseRadiusFactor: + 0.3 + rand.nextDouble() * 0.1, // Slight base radius variation + speedFactor: 0.8 + rand.nextDouble() * 0.4, // Slight speed variation + opacityFactor: 0.3 + rand.nextDouble() * 0.1, // Base opacity variation + maxSweep: + math.pi / 1.5 + rand.nextDouble() * (math.pi / 2), // Random max sweep + ); + } + + final double phaseOffset; + final double baseRadiusFactor; + final double speedFactor; + final double opacityFactor; + final double maxSweep; +} + +/// Draws dream-like swirling latte art. +class EmptyLatteArt extends StatefulWidget { + /// Instantiates an [EmptyLatteArt]. + const EmptyLatteArt({super.key}); + + @override + State createState() => _EmptyLatteArtState(); +} + +class _EmptyLatteArtState extends State + with SingleTickerProviderStateMixin { + late Ticker _ticker; + + // ValueNotifier is a highly efficient way to rebuild only the CustomPaint + // without rebuilding the entire widget tree on every frame. + final ValueNotifier _time = ValueNotifier(0); + late List<_ArcConfig> _arcConfigs; + + static const _swirlSpeed = 12; + + @override + void initState() { + super.initState(); + _arcConfigs = List.generate(6, (index) => _ArcConfig.random()); + + // Create a ticker that fires every frame. + // It provides the total elapsed time, which grows infinitely and never + // resets. + _ticker = createTicker((Duration elapsed) { + // Use microseconds for high-precision, perfectly smooth double values + _time.value = elapsed.inMicroseconds / Duration.microsecondsPerSecond; + }); + + _ticker.start().ignore(); + } + + @override + void dispose() { + _ticker.dispose(); + _time.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final size = math.min(constraints.maxWidth, constraints.maxHeight); + return SizedBox( + width: size, + height: size, + child: ValueListenableBuilder( + valueListenable: _time, + builder: (context, time, child) { + return CustomPaint( + painter: _LattePainter(time / _swirlSpeed, _arcConfigs), + ); + }, + ), + ); + }, + ); + } +} + +class _LattePainter extends CustomPainter { + _LattePainter(this.progress, this.arcConfigs); + final double progress; + final List<_ArcConfig> arcConfigs; + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = math.min(size.width, size.height) / 2; + + // --- LAYER 1: Large, Blurry Outer Circle (Outline) --- + final outlinePaint = Paint() + ..color = + const Color(0xFF4E342E) // Deep espresso brown + ..style = PaintingStyle.fill + // Increased blur for very soft outline + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 30); + + // Draw slightly larger than radius to cover edges + canvas.drawCircle(center, radius * 1.05, outlinePaint); + + // --- LAYER 2: Solid Inner Circle --- + final softInnerPaint = Paint() + ..color = + const Color(0xFF4E342E) // Same deep brown + ..style = PaintingStyle.fill + // ADDED: A moderate blur to melt the hard edge into the outer layer + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2); + + // Draw inner circle. The radius stays the same, but the edge is now soft. + canvas.drawCircle(center, radius * 0.95, softInnerPaint); + + // --- LAYER 3: Swirling, Morphing Arcs (Inside Solid Circle) --- + // Setup the "foam" paint (reduce arc blur slightly to make them "less + // blurred") + final foamPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + // Maintain some blur for dreamy feel, but less than outer circle blur + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6); + + // Draw multiple swirling, morphing arcs using unique instance data + for (int i = 0; i < arcConfigs.length; i++) { + final config = arcConfigs[i]; + + // Use unique phase offset & speed for starting angle, incorporating + // global progress + final startAngle = + (progress * math.pi * 4 * config.speedFactor) + config.phaseOffset; + + // Oscillate sweep, based on unique max sweep and progress/phase + final sweepAngle = + (config.maxSweep / 1.5) + + math.sin(progress * math.pi * 2 + config.phaseOffset) * + (config.maxSweep / 3); + + // Oscillate radius, using unique base radius factor and variation + double currentRadius = + radius * config.baseRadiusFactor + + (i * radius * 0.08) + + math.cos(progress * math.pi * 4 + config.phaseOffset) * 8.0; + + // Ensure arcs stay contained mostly within solid inner circle + // (adjust slightly) + if (currentRadius > radius * 0.93) currentRadius = radius * 0.93; + + // Morphing thickness (less per-instance variation here, but still morphs + // over time) + foamPaint.strokeWidth = + 10.0 + 8.0 * math.cos(progress * math.pi * 2 + config.phaseOffset); + + // Morphing opacity (using unique opacity factor and base phase for + // uniqueness) + final opacity = + config.opacityFactor + + 0.4 * math.sin(progress * math.pi * 2 + config.phaseOffset).abs(); + foamPaint.color = const Color(0xFFD7CCC8).withValues(alpha: opacity); + + // Draw the arc + final rect = Rect.fromCircle(center: center, radius: currentRadius); + canvas.drawArc(rect, startAngle, sweepAngle, false, foamPaint); + } + + // --- Central Swirling Blob (Optional, make slightly unique) --- + // Use data from one arc config to introduce slight per-instance variation + // in center + final centralRandFactor = arcConfigs[0].phaseOffset * 0.1; + final centerFoamPaint = Paint() + ..color = const Color(0xFFEFEBE9).withValues( + alpha: 0.4 + 0.2 * math.sin(progress * math.pi * 2 + centralRandFactor), + ) + ..style = PaintingStyle.fill + ..maskFilter = const MaskFilter.blur( + BlurStyle.normal, + 16, + ); // Maintain blur for center + + canvas.drawOval( + Rect.fromCenter( + center: center, + width: + radius * 0.6 + + math.cos(progress * math.pi * 2 + centralRandFactor) * 10, + height: + radius * 0.4 + + math.sin(progress * math.pi * 2 + centralRandFactor) * 10, + ), + centerFoamPaint, + ); + } + + @override + bool shouldRepaint(covariant _LattePainter oldDelegate) { + // Re-paint when progress changes (animation) or if configs differ (not + // usually applicable per instance, but safer check) + return oldDelegate.progress != progress || + oldDelegate.arcConfigs != arcConfigs; + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/loading_progress.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/loading_progress.dart new file mode 100644 index 0000000..de6e903 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/loading_progress.dart @@ -0,0 +1,459 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:genlatte/src/screens/app/theme.dart'; +import 'package:genlatte/src/widgets/loading_dash.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// A widget that only shows itself when [stillWaiting] is true, +/// and fades out otherwise (ignoring pointers). It is therefore safe +/// to have this widget in a [Stack] above other widgets. +/// +/// The widget shows an animation ([LoadingDash]) and a progress bar +/// below it. It is designed to take the user's mind away from the fact +/// they are waiting. +/// +/// Note that the progress bar is a lie. We _don't_ actually know how far +/// in the process we are at any point. All we know is the [expectedDuration] +/// and [worstCaseDuration]. +class LoadingProgress extends StatefulWidget { + /// Creates a new [LoadingProgress]. + const LoadingProgress({ + required this.progressStartTime, + required this.padding, + required this.stillWaiting, + this.expectedDuration = const Duration(seconds: 30), + this.worstCaseDuration = const Duration(minutes: 1, seconds: 20), + this.primaryColor = AppColors.chevronYellow, + this.secondaryColor = AppColors.latteArtGold, + super.key, + }) : assert( + worstCaseDuration > expectedDuration, + 'Worst case duration must be longer than expected duration', + ); + + /// The expected duration of the loading process. + /// + /// Choose a duration at the top of the expected range so that the user + /// is more likely to be pleasantly surprised rather than infuriated. + /// 90% of the users should see the action completed _sooner_ than + /// the given duration. + final Duration expectedDuration; + + /// 99.9% of users should see the action completed before this time. + /// After that, the progress bar no longer moves. + /// + /// This _MUST_ be higher than [expectedDuration]. + final Duration worstCaseDuration; + + /// The time at which this progress bar started. + /// + /// This exists in case the widget needs to go in and out of the widget tree + /// but we don't want to restart the progress each time. + final DateTime? progressStartTime; + + /// This is `true` if the loading progress should be shown. + /// + /// When set to `false`, the widget will immediately stop responding to + /// pointer events, will start fading away, and after that, will + /// remove its children from the widget tree. + final bool stillWaiting; + + /// Padding for the main content of the progress indicator. + final EdgeInsets padding; + + /// The main color of the progress bar. + final Color primaryColor; + + /// Accent color (used for the border around the progress bar). + final Color secondaryColor; + + @override + State createState() => _LoadingProgressState(); +} + +class _LoadingProgressState extends State + with SingleTickerProviderStateMixin { + late final AnimationController opacityController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 1000), + ); + + late DateTime startTime; + + DateTime _latestTime = DateTime.now(); + + double _progress = 0; + + Timer? _timer; + + /// A 'designed' curve that transforms the real (linear) progress to + /// a non-linear curve that 'vibes' better with the human psyche. + /// See [_recomputeProgressTween]. + late TweenSequence progressTween; + + /// This becomes `true` only when [LoadingProgress.stillWaiting] is `true` + /// and the [opacityController]'s value is non-zero (it is not dismissed). + bool _visible = false; + + @override + Widget build(BuildContext context) { + final progress = widget.stillWaiting + ? progressTween.transform(_progress.clamp(0, 1)) + : 1.0; + + final screenWidth = MediaQuery.widthOf(context); + final tickerScaler = TextScaler.linear(min(1, screenWidth / 600)); + + return Visibility( + // Avoid paying the price for showing the widget subtree below + // (incl. the Dash movie). + visible: _visible, + child: IgnorePointer( + // As soon as we start fading out, ignore pointer. + ignoring: widget.stillWaiting, + child: Padding( + padding: widget.padding, + child: FadeTransition( + opacity: opacityController, + child: Column( + crossAxisAlignment: .stretch, + children: [ + const Expanded(child: LoadingDash()), + const SizedBox(height: 40), + Stack( + children: [ + _ProgressBar( + progress: progress, + primaryColor: widget.primaryColor, + secondaryColor: widget.secondaryColor, + ), + Positioned.fill( + child: Center( + child: WrappedText( + style: (context, theme) => + theme.typography.p.copyWith( + // Make the ticker text blend as a difference + // of the background. + foreground: Paint() + ..color = const Color(0xFFFFFFFF) + ..blendMode = BlendMode.difference, + fontSize: tickerScaler.scale( + theme.typography.p.fontSize!, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _Ticker( + sinceStart: _latestTime.difference(startTime), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + @override + void didUpdateWidget(covariant LoadingProgress oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.progressStartTime != null && + widget.progressStartTime != oldWidget.progressStartTime) { + startTime = widget.progressStartTime!; + } + + if (widget.stillWaiting && !oldWidget.stillWaiting) { + _start(); + unawaited(opacityController.forward()); + _visible = true; + } else if (!widget.stillWaiting && oldWidget.stillWaiting) { + _stop(); + unawaited(opacityController.reverse()); + } + + _recomputeProgressTween(); + } + + @override + void dispose() { + _timer?.cancel(); + opacityController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + startTime = widget.progressStartTime ?? DateTime.now(); + _recomputeProgressTween(); + + opacityController.addStatusListener(_onAnimationStatusChange); + + if (widget.stillWaiting) { + _start(); + unawaited(opacityController.forward()); + _visible = true; + } + } + + void _onAnimationStatusChange(AnimationStatus status) { + if (status == .dismissed && !widget.stillWaiting) { + _visible = false; + } + } + + void _recomputeProgressTween() { + progressTween = TweenSequence([ + // Start subtly faster. + TweenSequenceItem( + tween: Tween(begin: 0.01, end: 0.15), + weight: 0.10 * widget.expectedDuration.inMilliseconds, + ), + // Most of the progression is mostly gradual. + TweenSequenceItem( + tween: Tween(begin: 0.15, end: 0.90), + weight: 0.90 * widget.expectedDuration.inMilliseconds, + ), + // Slow down at the very end. Hopefully, most users don't see this. + TweenSequenceItem( + tween: Tween(begin: 0.90, end: 0.98), + weight: + 0.5 * + (widget.worstCaseDuration - widget.expectedDuration).inMilliseconds, + ), + // The very last leg where almost no progress is achieved. + TweenSequenceItem( + tween: Tween(begin: 0.98, end: 0.995), + weight: + 0.5 * + (widget.worstCaseDuration - widget.expectedDuration).inMilliseconds, + ), + ]); + } + + void _start() { + _timer?.cancel(); + _timer = Timer.periodic(const Duration(milliseconds: 500), _tick); + _tick(null); + } + + void _stop() { + _timer?.cancel(); + } + + void _tick(Object? _) { + if (!mounted) return; + _latestTime = DateTime.now(); + final elapsed = _latestTime.difference(startTime); + setState(() { + _progress = + elapsed.inMilliseconds / widget.worstCaseDuration.inMilliseconds; + }); + } +} + +class _ProgressBar extends StatefulWidget { + const _ProgressBar({ + required this.progress, + required this.primaryColor, + required this.secondaryColor, + }); + + final double progress; + + final Color primaryColor; + + final Color secondaryColor; + + @override + State<_ProgressBar> createState() => _ProgressBarState(); +} + +class _ProgressBarState extends State<_ProgressBar> + with SingleTickerProviderStateMixin<_ProgressBar> { + late final animationController = AnimationController( + vsync: this, + ); + + final Duration stepDuration = const Duration(milliseconds: 300); + + final Curve stepCurve = Curves.easeOut; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 40, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all(color: widget.primaryColor), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: AnimatedBuilder( + animation: animationController, + builder: (context, child) => FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: animationController.value, + child: child, + ), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: widget.secondaryColor, + ), + ), + ), + ), + ), + ); + } + + @override + void didUpdateWidget(covariant _ProgressBar oldWidget) { + super.didUpdateWidget(oldWidget); + unawaited( + animationController.animateTo( + widget.progress.clamp(0, 1), + duration: stepDuration, + curve: stepCurve, + ), + ); + } + + @override + void dispose() { + animationController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + unawaited( + animationController.animateTo( + widget.progress.clamp(0, 1), + duration: stepDuration, + curve: stepCurve, + ), + ); + } +} + +class _Ticker extends StatelessWidget { + const _Ticker({ + required this.sinceStart, + // ignore: unused_element_parameter + this.messages = _defaultMessages, + this.switchDuration = const Duration(seconds: 3), + }) : assert( + switchDuration > Duration.zero, + 'Switch duration must be non-zero', + ); + + static const List _defaultMessages = [ + 'Generating four images in parallel', + 'Expanding prompts via Gemini Flash', + 'Tokenizing input prompts', + 'Calculating Gemini multimodal embeddings', + 'Encoding text prompts into latent space', + 'Peeling initial nanobananas', + 'Sampling from Gaussian noise distribution', + 'Injecting prompt embeddings into the diffusion process', + 'Calibrating nanobanana attention heads', + 'Aligning embeddings with visual features', + 'Executing reverse diffusion steps', + 'Decoding latents into pixel space', + 'Refining high-frequency details', + 'Scaling resolution via variational autoencoder', + 'Embedding SynthID provenance metadata', + 'Converging on final pixel arrangements', + // At this point, most users are away. The following is to give some + // entertainment to the poor souls who need to wait longer. + 'Hallucinating 100% progress', + 'Reticulating splines', + 'Constructing additional pylons', + 'Rerouting power from the holodeck', + 'Trying to exit vim', + 'Reversing the polarity of the neutron flow', + 'Gathering your party before venturing forth', + 'Compensating for Heisenberg compensators', + "Saying: \"I'm sorry, Dave. I'm afraid I can't do that.\"", + 'Downloading more RAM', + 'Establishing a secure connection to Skynet', + 'Dividing by zero (as a treat)', + 'Warming up the flux capacitor', + 'Bypassing the Kobayashi Maru scenario', + 'Executing `sudo rm -rf /`', + 'Calculating the answer to life, the universe, and everything', + 'Resolving circular dependencies', + 'Blowing into the cartridge', + 'Commenting out broken unit tests', + 'Scanning for Cylon raiders', + 'Defragmenting the mainframe', + 'Punching widget trees', + 'Equipping +5 GPU of Rendering', + 'Ensuring the flow of spice', + 'Ignoring the warning signs in the logs', + 'Calculating PageRank', + 'Requisitioning a bigger boat', + 'Executing order 66', + 'Turning the TPUs up to eleven', + 'Going ahead and making my day', + 'Turning it off and on again', + '', + 'Ah shucks, here we go again...', + ]; + + final Duration sinceStart; + + final Duration switchDuration; + + final List messages; + + @override + Widget build(BuildContext context) { + final index = + (sinceStart.inMilliseconds ~/ switchDuration.inMilliseconds) % + messages.length; + final message = messages[index]; + + return ClipRect( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchOutCurve: Curves.ease, + switchInCurve: Curves.ease, + // The default FadeTransition doesn't work well with + // BlendMode.difference so we're sliding the text up and down. + // The ClipRect above makes sure the text disappears from view. + transitionBuilder: (child, animation) => SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: Text( + message, + // Key must be provided for AnimatedSwitcher to work. + key: ValueKey(message), + maxLines: 1, + overflow: .ellipsis, + ), + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/segmented_progress.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/segmented_progress.dart new file mode 100644 index 0000000..ffa2aa5 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/segmented_progress.dart @@ -0,0 +1,107 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// A horizontal progress bar that is segmented into multiple steps, with the +/// current step being indicated by increased width. +class SegmentedProgress extends StatelessWidget { + /// Creates a new [SegmentedProgress]. + const SegmentedProgress({ + required this.currentStep, + required this.totalSteps, + this.inactiveHeight = 8.0, + this.activeHeight = 20.0, + this.maxWidth = 220, + super.key, + }) : assert( + currentStep < totalSteps, + 'currentStep must be less than totalSteps; you have an off-by-1 error', + ); + + /// The current step. + final int currentStep; + + /// The total number of steps. + final int totalSteps; + + /// The height of all steps whose index does not equal [currentStep]. + final double inactiveHeight; + + /// The height of the step whose index equals [currentStep]. + final double activeHeight; + + /// The maximum width of the progress bar. + final double maxWidth; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: maxWidth, + height: activeHeight, + child: _SegmentedProgressInner( + currentStep: currentStep, + totalSteps: totalSteps, + inactiveHeight: inactiveHeight, + activeHeight: activeHeight, + ), + ); + } +} + +class _SegmentedProgressInner extends StatelessWidget { + const _SegmentedProgressInner({ + required this.currentStep, + required this.totalSteps, + required this.inactiveHeight, + required this.activeHeight, + }); + + /// The current step. + final int currentStep; + + /// The total number of steps. + final int totalSteps; + + /// The height of all steps whose index does not equal [currentStep]. + final double inactiveHeight; + + /// The height of the step whose index equals [currentStep]. + final double activeHeight; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final totalWidth = constraints.maxWidth; + final segmentWidth = (totalWidth * 0.92) / totalSteps; + + return Row( + mainAxisAlignment: .spaceBetween, + children: List.generate(totalSteps, (index) { + final height = index == currentStep ? activeHeight : inactiveHeight; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + width: segmentWidth, + height: height, + decoration: ShapeDecoration( + color: index <= currentStep + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.muted, + shape: const StadiumBorder(), + ), + ); + }), + ); + }, + ).animate().fadeIn( + // 100ms per item on the intro card, of which there are 4 + // 100ms being the outro speed of each ZipItem's animation + delay: const Duration(milliseconds: 400), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/widgets.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/widgets.dart new file mode 100644 index 0000000..928c622 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/widgets.dart @@ -0,0 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'empty_latte_art.dart'; +export 'segmented_progress.dart'; +export 'zip_page_view.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/zip_item.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/zip_item.dart new file mode 100644 index 0000000..7eb32ae --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/zip_item.dart @@ -0,0 +1,73 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/screens/kiosk/widgets/widgets.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// An item that animates in or out on its own timing based on its [index]. +class ZipItem extends StatefulWidget { + /// Instantiates a new [ZipItem]. + const ZipItem({ + required this.index, + required this.child, + super.key, + }); + + /// Order of this item's animation in or out. + final int index; + + /// Child widget to animate. + final Widget child; + + @override + State createState() => _ZipItemState(); +} + +class _ZipItemState extends State { + Animation? _animation; + ZipPageState? _pageState; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Register with parent to get staggered timing + _pageState = ZipPage.of(context); + if (_pageState != null) { + _pageState!.registerItem(widget.index); + _animation = _pageState!.getItemAnimation(widget.index); + } + } + + @override + Widget build(BuildContext context) { + if (_animation == null || _pageState == null) return widget.child; + + return AnimatedBuilder( + animation: _animation!, + builder: (context, child) { + // Value 0.0 = Offscreen + // Value 1.0 = OnScreen + + final screenWidth = MediaQuery.sizeOf(context).width; + + // When value is 0, we want to be at offset (screenWidth * direction) + // When value is 1, we want to be at 0 + + // Invert value for translation calculation + final value = _animation!.value; + final double translation = + (_pageState!.flyDirection * screenWidth) * (1 - value); + + // Add a slight opacity fade for smoothness + final double opacity = value.clamp(0, 1); + + return Transform.translate( + offset: Offset(translation, 0), + child: Opacity(opacity: opacity, child: child), + ); + }, + child: widget.child, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/zip_page_view.dart b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/zip_page_view.dart new file mode 100644 index 0000000..33e855f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/kiosk/widgets/zip_page_view.dart @@ -0,0 +1,277 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport 'package:genlatte/src/screens/kiosk/widgets/zip_item.dart'; +library; + +import 'dart:math' as math; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Manages a series of pages with sub-items that animate in and out +/// one item at a time. +/// +/// Individual sub-items are wrapped in a [ZipItem] widget, each of which +/// accepts an integer `index` parameter. These indicies must appear in +/// descending order within the wrapping widget's build method. Behavior is +/// undefined if they are registered in any other order. The reason for this +/// is that the [ZipPage] widget must immediately know the maximum number of +/// children so that the first widget to appear can receive an [Animation] with +/// the correct total duration. +class ZipPageView extends StatefulWidget { + /// Instantiates a new [ZipPageView]. + const ZipPageView({ + required this.builder, + required this.itemCount, + required this.currentIndex, + required this.onNewPage, + super.key, + }); + + /// The pages to display. + final IndexedWidgetBuilder builder; + + /// The number of pages to display. + final int itemCount; + + /// The current index of the page. + final int currentIndex; + + /// Callback for when a new page is about to be displayed. This is not called + /// until after the previous page is gone, meaning it is safe to remove state + /// associated with the previous page or to load data for the new page. + final void Function(int) onNewPage; + + @override + State createState() => _ZipPageViewState(); +} + +class _ZipPageViewState extends State { + late PageController _pageController; + late int _currentIndex; + late int _previousIndex; + + // Updated to either reflect _previousIndex or _currentIndex depending on + // whether we are in the first or second half of the transition animation. + late int _indexToBuild; + + bool _isAnimating = false; + + // By default, all pages animate-in. However, that would cause the first page + // to flicker; so this flag allows the first page to start fully in-place. + bool _hasNavigated = false; + + final Map> _pageKeys = {}; + + @override + void initState() { + super.initState(); + _currentIndex = widget.currentIndex; + _indexToBuild = _currentIndex; + _pageController = PageController(initialPage: _currentIndex); + } + + @override + void didUpdateWidget(covariant ZipPageView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.currentIndex != oldWidget.currentIndex) { + _previousIndex = _currentIndex; + _currentIndex = widget.currentIndex; + _handleTransition(widget.currentIndex - oldWidget.currentIndex).ignore(); + } + } + + GlobalKey _getKey(int index) { + return _pageKeys.putIfAbsent(index, GlobalKey.new); + } + + Future _handleTransition(int direction) async { + if (_isAnimating) return; + + if (_currentIndex < 0 || _currentIndex >= widget.itemCount) { + return; + } + + setState(() { + _isAnimating = true; + _hasNavigated = true; // Mark that future builds should start hidden + }); + + // 1. Exit current page + final animatingOutPageKey = _getKey(_previousIndex); + if (animatingOutPageKey.currentState != null) { + await animatingOutPageKey.currentState!._animateExit(direction); + _indexToBuild = _currentIndex; + } + + // 2. Move Controller + _pageController.jumpToPage(_currentIndex); + + // 3. Wait for build (imperative vs declarative sync) + // We need the new page to mount in its "hidden" state before we animate + // it in. + await Future.delayed(const Duration(milliseconds: 50)); + + // 4. Enter new page + final nextPageKey = _getKey(_currentIndex); + if (nextPageKey.currentState != null) { + widget.onNewPage(_currentIndex); + await nextPageKey.currentState!._animateEntrance(direction); + } + + setState(() => _isAnimating = false); + } + + @override + Widget build(BuildContext context) { + return PageView.builder( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget.itemCount, + itemBuilder: (context, index) { + // Determine initial visibility + final bool isInitialPage = index == 0 && !_hasNavigated; + + return ZipPage( + key: _getKey(index), + startVisible: isInitialPage, + child: widget.builder(context, _indexToBuild), + ); + }, + ); + } +} + +/// A page that animates in and out one item at a time. +class ZipPage extends StatefulWidget { + /// Instantiates a new [ZipPage]. + const ZipPage({ + required this.child, + super.key, + this.startVisible = true, + }); + + /// The child widget to display. + final Widget child; + + /// Differentiates the initial page, which starts visible, from subsequent + /// pages, which are invisible until animated-in. + final bool startVisible; + + @override + State createState() => ZipPageState(); + + /// Retrieves the [ZipPageState] for the nearest [ZipPage] ancestor. + static ZipPageState? of(BuildContext context) { + return context.findAncestorStateOfType(); + } +} + +/// The state of a [ZipPage]. +class ZipPageState extends State with TickerProviderStateMixin { + late AnimationController _controller; + int _childCount = 0; + + final Duration _itemDuration = const Duration(milliseconds: 400); + final Duration _staggerDelay = const Duration(milliseconds: 100); + + // Default fly direction + double _flyDirection = 1; + + int? _highestIndexRegistered; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + value: widget.startVisible ? 1 : 0, + duration: const Duration(milliseconds: 1000), + ); + } + + /// Allow dynamic updates to duration as items register + void registerItem(int index) { + _childCount++; + _updateDuration(index); + } + + void _updateDuration(int index) { + if (_highestIndexRegistered == null) { + _highestIndexRegistered = index; + } else { + _highestIndexRegistered = math.max(_highestIndexRegistered!, index); + } + final totalMs = + (_highestIndexRegistered! * _staggerDelay.inMilliseconds) + + _itemDuration.inMilliseconds; + _controller.duration = Duration(milliseconds: totalMs); + } + + Future _animateExit(int swipeDirection) async { + _flyDirection = -swipeDirection.toDouble(); + await _controller.reverse(); // Goes from 1.0 to 0.0 + } + + Future _animateEntrance(int swipeDirection) async { + _flyDirection = swipeDirection.toDouble(); + + // Ensure we start at 0 (Hidden) + _controller.value = 0.0; + + // Animate to 1 (Visible) + await _controller.forward(); + } + + /// Retrieves the animation for the given item index. + Animation getItemAnimation(int index) { + if (_childCount == 0) return const AlwaysStoppedAnimation(1); + + final totalMs = _controller.duration!.inMilliseconds; + + // EXITS (Reverse curve) + // The current/original behavior: descending index order -> left-most out first. + final exitStartMs = index * _staggerDelay.inMilliseconds; + final exitEndMs = exitStartMs + _itemDuration.inMilliseconds; + + // ENTRANCES (Forward curve) + // Invert the index so that left-most (highest index) animates in first, + // then next right-most, etc. + // _highestIndexRegistered is guaranteed to be correct here since indices + // must be registered in descending order. + final invertedIndex = (_highestIndexRegistered ?? index) - index; + final entranceStartMs = invertedIndex * _staggerDelay.inMilliseconds; + final entranceEndMs = entranceStartMs + _itemDuration.inMilliseconds; + + return Tween(begin: 0, end: 1).animate( + CurvedAnimation( + parent: _controller, + curve: Interval( + entranceStartMs / totalMs, + math.min(1, entranceEndMs / totalMs), + curve: Curves.easeOutExpo, + ), + reverseCurve: Interval( + exitStartMs / totalMs, + math.min(1, exitEndMs / totalMs), + curve: Curves.easeOutExpo, + ), + ), + ); + } + + /// The direction of the fly animation. + double get flyDirection => _flyDirection; + + @override + Widget build(BuildContext context) { + _childCount = 0; // Reset count for rebuilds + return widget.child; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/login/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/login/CONTEXT.md new file mode 100644 index 0000000..182d1f5 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/login/CONTEXT.md @@ -0,0 +1,20 @@ +# Login Screen + +**Purpose:** +Provides the authentication entry point interface across all user personas. + +**Detailed File Overviews:** + +- `login.dart`: + - **Description**: Barrel export file. + +- `login_view.dart`: + - **Description**: The visual component for signing in. + - **Core Logic**: Presents a stylized animated Google log-in button. Depending on the environment configurations, it may trigger the `FirebaseAuth.instance.signInWithPopup` using Google Auth providers. + +- `login_bloc.dart`: + - **Description**: State management backing the Login screen. + - **Core Logic**: Intercepts `GoogleLoginEvent` triggers, managing the transition between loading spinners and error states. If authentication fails, it yields customized error states back to the view for `shadcn_flutter` toasts. + +**Dependencies/Relationships:** +- Tied to `core/auth/auth_bloc` indirectly (since `AuthBloc` intercepts the actual `User` object generated by Google signing in here and propagates the redirects). diff --git a/genlatte/genlatte-ui/lib/src/screens/login/login.dart b/genlatte/genlatte-ui/lib/src/screens/login/login.dart new file mode 100644 index 0000000..f0d914f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/login/login.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'login_bloc.dart'; +export 'login_view.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/login/login_bloc.dart b/genlatte/genlatte-ui/lib/src/screens/login/login_bloc.dart new file mode 100644 index 0000000..3331a79 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/login/login_bloc.dart @@ -0,0 +1,74 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:logging/logging.dart'; + +part 'login_bloc.freezed.dart'; + +final _log = Logger('LoginBloc'); + +typedef _Emit = Emitter; + +/// {@template LoginBloc} +/// {@endtemplate} +class LoginBloc extends Bloc { + /// {@macro LoginBloc} + LoginBloc(this._authService) : super(LoginState.initial()) { + on( + (event, _Emit emit) => switch (event) { + LoginUserEvent() => _onLoginUser(event, emit), + }, + ); + } + final FirebaseAuth _authService; + + Future _onLoginUser(LoginUserEvent event, _Emit emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + try { + await _authService.signInWithEmailAndPassword( + email: event.email, + password: event.password, + ); + } on FirebaseAuthException catch (e) { + _log.severe( + 'Failed to sign in with FirebaseAuthException: $e :: ${e.message}', + ); + emit(state.copyWith(errorMessage: e.message, isLoading: false)); + } on Exception catch (e) { + _log.severe('Failed to sign in with Exception: $e'); + emit(state.copyWith(errorMessage: e.toString(), isLoading: false)); + } + } +} + +/// Actions that can be taken on the Login page. +@Freezed() +sealed class LoginEvent with _$LoginEvent { + /// Submitting an email and password. + const factory LoginEvent.login({ + required String email, + required String password, + }) = LoginUserEvent; +} + +/// {@template LoginState} +/// Complete representation of the Login page's state. +/// {@endtemplate +@Freezed() +sealed class LoginState with _$LoginState { + /// {@macro LoginState} + const factory LoginState({ + String? email, + String? password, + String? errorMessage, + @Default(false) bool isLoading, + }) = _LoginState; + const LoginState._(); + + /// Starter state fed to the [LoginBloc]. + factory LoginState.initial() => const LoginState(); +} diff --git a/genlatte/genlatte-ui/lib/src/screens/login/login_bloc.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/login/login_bloc.freezed.dart new file mode 100644 index 0000000..272f154 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/login/login_bloc.freezed.dart @@ -0,0 +1,532 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'login_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$LoginEvent { + + String get email; String get password; +/// Create a copy of LoginEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LoginEventCopyWith get copyWith => _$LoginEventCopyWithImpl(this as LoginEvent, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LoginEvent&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)); +} + + +@override +int get hashCode => Object.hash(runtimeType,email,password); + +@override +String toString() { + return 'LoginEvent(email: $email, password: $password)'; +} + + +} + +/// @nodoc +abstract mixin class $LoginEventCopyWith<$Res> { + factory $LoginEventCopyWith(LoginEvent value, $Res Function(LoginEvent) _then) = _$LoginEventCopyWithImpl; +@useResult +$Res call({ + String email, String password +}); + + + + +} +/// @nodoc +class _$LoginEventCopyWithImpl<$Res> + implements $LoginEventCopyWith<$Res> { + _$LoginEventCopyWithImpl(this._self, this._then); + + final LoginEvent _self; + final $Res Function(LoginEvent) _then; + +/// Create a copy of LoginEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? email = null,Object? password = null,}) { + return _then(_self.copyWith( +email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [LoginEvent]. +extension LoginEventPatterns on LoginEvent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( LoginUserEvent value)? login,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case LoginUserEvent() when login != null: +return login(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( LoginUserEvent value) login,}){ +final _that = this; +switch (_that) { +case LoginUserEvent(): +return login(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( LoginUserEvent value)? login,}){ +final _that = this; +switch (_that) { +case LoginUserEvent() when login != null: +return login(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( String email, String password)? login,required TResult orElse(),}) {final _that = this; +switch (_that) { +case LoginUserEvent() when login != null: +return login(_that.email,_that.password);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( String email, String password) login,}) {final _that = this; +switch (_that) { +case LoginUserEvent(): +return login(_that.email,_that.password);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( String email, String password)? login,}) {final _that = this; +switch (_that) { +case LoginUserEvent() when login != null: +return login(_that.email,_that.password);case _: + return null; + +} +} + +} + +/// @nodoc + + +class LoginUserEvent implements LoginEvent { + const LoginUserEvent({required this.email, required this.password}); + + +@override final String email; +@override final String password; + +/// Create a copy of LoginEvent +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LoginUserEventCopyWith get copyWith => _$LoginUserEventCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LoginUserEvent&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)); +} + + +@override +int get hashCode => Object.hash(runtimeType,email,password); + +@override +String toString() { + return 'LoginEvent.login(email: $email, password: $password)'; +} + + +} + +/// @nodoc +abstract mixin class $LoginUserEventCopyWith<$Res> implements $LoginEventCopyWith<$Res> { + factory $LoginUserEventCopyWith(LoginUserEvent value, $Res Function(LoginUserEvent) _then) = _$LoginUserEventCopyWithImpl; +@override @useResult +$Res call({ + String email, String password +}); + + + + +} +/// @nodoc +class _$LoginUserEventCopyWithImpl<$Res> + implements $LoginUserEventCopyWith<$Res> { + _$LoginUserEventCopyWithImpl(this._self, this._then); + + final LoginUserEvent _self; + final $Res Function(LoginUserEvent) _then; + +/// Create a copy of LoginEvent +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? email = null,Object? password = null,}) { + return _then(LoginUserEvent( +email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc +mixin _$LoginState { + + String? get email; String? get password; String? get errorMessage; bool get isLoading; +/// Create a copy of LoginState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LoginStateCopyWith get copyWith => _$LoginStateCopyWithImpl(this as LoginState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LoginState&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)); +} + + +@override +int get hashCode => Object.hash(runtimeType,email,password,errorMessage,isLoading); + +@override +String toString() { + return 'LoginState(email: $email, password: $password, errorMessage: $errorMessage, isLoading: $isLoading)'; +} + + +} + +/// @nodoc +abstract mixin class $LoginStateCopyWith<$Res> { + factory $LoginStateCopyWith(LoginState value, $Res Function(LoginState) _then) = _$LoginStateCopyWithImpl; +@useResult +$Res call({ + String? email, String? password, String? errorMessage, bool isLoading +}); + + + + +} +/// @nodoc +class _$LoginStateCopyWithImpl<$Res> + implements $LoginStateCopyWith<$Res> { + _$LoginStateCopyWithImpl(this._self, this._then); + + final LoginState _self; + final $Res Function(LoginState) _then; + +/// Create a copy of LoginState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? email = freezed,Object? password = freezed,Object? errorMessage = freezed,Object? isLoading = null,}) { + return _then(_self.copyWith( +email: freezed == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String?,password: freezed == password ? _self.password : password // ignore: cast_nullable_to_non_nullable +as String?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [LoginState]. +extension LoginStatePatterns on LoginState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LoginState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LoginState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LoginState value) $default,){ +final _that = this; +switch (_that) { +case _LoginState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LoginState value)? $default,){ +final _that = this; +switch (_that) { +case _LoginState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? email, String? password, String? errorMessage, bool isLoading)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LoginState() when $default != null: +return $default(_that.email,_that.password,_that.errorMessage,_that.isLoading);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? email, String? password, String? errorMessage, bool isLoading) $default,) {final _that = this; +switch (_that) { +case _LoginState(): +return $default(_that.email,_that.password,_that.errorMessage,_that.isLoading);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? email, String? password, String? errorMessage, bool isLoading)? $default,) {final _that = this; +switch (_that) { +case _LoginState() when $default != null: +return $default(_that.email,_that.password,_that.errorMessage,_that.isLoading);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _LoginState extends LoginState { + const _LoginState({this.email, this.password, this.errorMessage, this.isLoading = false}): super._(); + + +@override final String? email; +@override final String? password; +@override final String? errorMessage; +@override@JsonKey() final bool isLoading; + +/// Create a copy of LoginState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LoginStateCopyWith<_LoginState> get copyWith => __$LoginStateCopyWithImpl<_LoginState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoginState&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)); +} + + +@override +int get hashCode => Object.hash(runtimeType,email,password,errorMessage,isLoading); + +@override +String toString() { + return 'LoginState(email: $email, password: $password, errorMessage: $errorMessage, isLoading: $isLoading)'; +} + + +} + +/// @nodoc +abstract mixin class _$LoginStateCopyWith<$Res> implements $LoginStateCopyWith<$Res> { + factory _$LoginStateCopyWith(_LoginState value, $Res Function(_LoginState) _then) = __$LoginStateCopyWithImpl; +@override @useResult +$Res call({ + String? email, String? password, String? errorMessage, bool isLoading +}); + + + + +} +/// @nodoc +class __$LoginStateCopyWithImpl<$Res> + implements _$LoginStateCopyWith<$Res> { + __$LoginStateCopyWithImpl(this._self, this._then); + + final _LoginState _self; + final $Res Function(_LoginState) _then; + +/// Create a copy of LoginState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? email = freezed,Object? password = freezed,Object? errorMessage = freezed,Object? isLoading = null,}) { + return _then(_LoginState( +email: freezed == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String?,password: freezed == password ? _self.password : password // ignore: cast_nullable_to_non_nullable +as String?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/login/login_view.dart b/genlatte/genlatte-ui/lib/src/screens/login/login_view.dart new file mode 100644 index 0000000..00ee091 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/login/login_view.dart @@ -0,0 +1,206 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/login/login.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// {@template LoginScreen} +/// Initial Login screen. +/// {@endtemplate} +class LoginScreen extends StatelessWidget { + /// {@macro LoginScreen} + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.white, + headers: [ + AppBar( + title: const Text('Login').h3, + height: 50, + ), + ], + child: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: const Column( + mainAxisAlignment: .center, + crossAxisAlignment: .stretch, + children: [ + _LoginForm(key: ValueKey('login_form')), + ], + ), + ), + ), + ), + ); + } +} + +class _LoginForm extends StatefulWidget { + const _LoginForm({super.key}); + + @override + State<_LoginForm> createState() => _LoginFormState(); +} + +class _LoginFormState extends State<_LoginForm> { + final TextFieldKey _emailKey = const TextFieldKey('email'); + final TextFieldKey _passwordKey = const TextFieldKey('password'); + + late final LoginBloc bloc; + bool _obscurePassword = true; + + @override + void initState() { + super.initState(); + bloc = LoginBloc(GetIt.I()); + } + + @override + void dispose() { + bloc.close().ignore(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + final rows = [ + FormField( + key: _emailKey, + label: const Text('Email').h3, + validator: const EmailValidator(), + child: const TextField( + autocorrect: false, + style: TextStyle(fontSize: 24), + ), + ), + FormField( + key: _passwordKey, + label: const Text('Password').h3, + validator: const LengthValidator(min: 8), + child: TextField( + autocorrect: false, + obscureText: _obscurePassword, + style: const TextStyle(fontSize: 24), + ), + ), + ]; + + return Theme( + data: themeData.copyWith( + colorScheme: () => themeData.colorScheme.copyWith( + border: () => AppColors.almostBlack, + ), + ), + child: ResponsiveSizedBox.builder( + aspectRatioClamp: (0.5, null, 2), + builder: (context, size) { + // True for narrower windows where we need to make better use + // of vertical space. + final labelsStacked = size.width < 800; + return Padding( + padding: labelsStacked + ? EdgeInsets.zero + : EdgeInsets.all(size.width * 0.05), + child: SizedBox( + width: labelsStacked ? max(480, size.width * 0.6) : size.width, + child: BlocBuilder( + bloc: bloc, + builder: (context, state) { + return Form( + onSubmit: (context, values) async { + final email = _emailKey[values]; + final password = _passwordKey[values]; + bloc.add( + LoginEvent.login( + email: email!, + password: password!, + ), + ); + }, + child: Column( + mainAxisSize: labelsStacked ? .max : .min, + crossAxisAlignment: labelsStacked + ? CrossAxisAlignment.stretch + : CrossAxisAlignment.end, + children: [ + if (labelsStacked) + Column( + crossAxisAlignment: .stretch, + children: rows, + ) + else + FormTableLayout(rows: rows), + const Gap(12), + Row( + children: [ + const Spacer(), + IconButton.ghost( + icon: Icon( + _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ], + ), + const Gap(24), + if (state.errorMessage != null) ...[ + Text( + state.errorMessage!, + style: const TextStyle( + color: AppColors.googleIntroRed, + ), + ).h4, + const Gap(24), + ], + FormErrorBuilder( + builder: (context, errors, child) { + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + // Disable the submit button while there are + // validation errors. + return PrimaryButton( + onPressed: errors.isEmpty + ? () => context.submitForm() + : null, + child: const Text( + 'Login', + style: TextStyle(fontSize: 24), + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ); + }, + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/moderator/CONTEXT.md new file mode 100644 index 0000000..04df99c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/CONTEXT.md @@ -0,0 +1,11 @@ +# Kiosk / Barista / Moderator / Queue / Recent Orders Roots + +**Purpose:** +This directory acts as the architectural boundary grouping for the application's distinct personas. + +**Implementation Details:** +- **Barrel Architecture**: This level strictly exports child module directories like `home/` and `widgets/` via an `index` / `barrel` file. +- **Child Contexts**: Please navigate into `home/` or `widgets/` subdirectories to view specific BLoC architectures, View layouts, and complex logic files tailored towards each app persona. + +**Dependencies:** +- Consumed by `core/routing/routes.dart` to map physical URLs to specific screen entry points. diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/home/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/moderator/home/CONTEXT.md new file mode 100644 index 0000000..b838fea --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/home/CONTEXT.md @@ -0,0 +1,22 @@ +# Moderator Home + +**Purpose:** +Provides the logic and UI for the Moderator dashboard. This screen aims to give moderators an overview of the entire latte bar, handling primarily the queue of submitted orders that require image or name safety moderation. + +**Detailed File Overviews:** + +- `moderator_home.dart`: + - **Description**: Barrel export file. + +- `moderator_home_bloc.dart`: + - **Description**: The BLoC managing moderator state via stream subscriptions. + - **Core Logic**: Subscribes to changes in `Barista` entities and `LatteOrderMetadata` using `RequestDetails(filter: LatteOrderMetadataModerationQueue())`. Exposes methods to handle approval or rejection flows for Names and Images via `_moderateOrder()` which toggles server validation status (`status: .validated`). It merges both moderation and brew queues sequentially to provide a full bar snapshot. + +- `moderator_home_view.dart`: + - **Description**: The primary UI for the Moderator Home screen. + - **Core Logic**: Constructs a Scaffold rendering an `InternalOrderQueues.moderator` instance. It pipes callbacks from the Queue cards into the Bloc events (`ApproveNameAndImage`, `RejectNameApproveImage`, `ApproveNameRejectImage`, `RejectNameAndImage`). Also includes an AppBar routing over to the `` via a print icon. + +**Dependencies/Relationships:** +- Closely tied to `genlatte_data` Models (`Barista`, `LatteOrderMetadata`). +- Relies heavily on `barista/widgets` (especially `InternalOrderQueues` and `LatteOrderCard.moderator`) for its core visualization. +- Connected to `AppRouter` to navigate to `machines/`. diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home.dart new file mode 100644 index 0000000..1f1cbfa --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'moderator_home_bloc.dart'; +export 'moderator_home_view.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home_bloc.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home_bloc.dart new file mode 100644 index 0000000..409275b --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home_bloc.dart @@ -0,0 +1,279 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte/src/data/data.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; + +part 'moderator_home_bloc.freezed.dart'; + +final _logger = Logger('ModeratorHomeBloc'); + +typedef _Emit = Emitter; + +/// {@template ModeratorHomeBloc} +/// Bloc for the Moderator home page. +/// {@endtemplate} +class ModeratorHomeBloc extends Bloc { + /// {@macro ModeratorHomeBloc} + ModeratorHomeBloc() : super(ModeratorHomeState.initial()) { + on( + (event, _Emit emit) => switch (event) { + NewBaristas() => _onNewBaristas(event, emit), + NewModerateQueueOrders() => _onNewModerateQueueOrders(event, emit), + ApproveNameAndImage() => _onApproveNameAndImage(event, emit), + RejectNameApproveImage() => _onRejectNameApproveImage(event, emit), + ApproveNameRejectImage() => _onApproveNameRejectImage(event, emit), + RejectNameAndImage() => _onRejectNameAndImage(event, emit), + CompleteOrder() => _onCompleteOrder(event, emit), + }, + ); + + _baristasRepository = GetIt.I>(); + _metadataRepository = GetIt.I>(); + _ordersRepository = GetIt.I(); + + _moderateStreamSub = _metadataRepository + .watchList( + details: RequestDetails.read( + filter: const LatteOrderMetadataModerationQueue(), + ), + ) + .listen((metadata) => add(NewModerateQueueOrders(metadata))); + + _baristaStreamSub = _baristasRepository.watchList().listen( + (baristas) => add(NewBaristas(baristas)), + ); + } + + late final Repository _baristasRepository; + late final LatteOrdersRepository _ordersRepository; + late final Repository _metadataRepository; + + late final StreamSubscription> _moderateStreamSub; + late final StreamSubscription> _baristaStreamSub; + + Future _onNewModerateQueueOrders( + NewModerateQueueOrders event, + _Emit emit, + ) async { + _logger.fine( + 'New moderation queue: ${event.metadatas.map((m) => m.id!)}', + ); + + List lattes; + try { + lattes = await _ordersRepository.toLattes(event.metadatas); + } on DataException catch (e) { + _logger.severe(e.message); + return; + } + + final moderationLattes = []; + final brewLattes = []; + + for (final latte in lattes) { + if (latte.metadata.status == LatteOrderStatus.submitted) { + moderationLattes.add(latte); + } else { + brewLattes.add(latte); + } + } + + emit( + state.copyWith( + moderationQueue: [ + // Older orders appear first + ...moderationLattes..sort( + (a, b) => a.metadata.orderSubmittedTime!.compareTo( + b.metadata.orderSubmittedTime!, + ), + ), + // Older orders appear first + ...brewLattes..sort( + (a, b) => a.metadata.orderSubmittedTime!.compareTo( + b.metadata.orderSubmittedTime!, + ), + ), + ], + ), + ); + } + + void _onNewBaristas(NewBaristas event, _Emit emit) { + _logger.fine('New baristas queue: ${event.baristas}'); + emit(state.copyWith(baristas: event.baristas.toIdsMap())); + } + + Future _onApproveNameAndImage( + ApproveNameAndImage event, + _Emit emit, + ) => _moderateOrder( + orderId: event.orderId, + isNameApproved: true, + isImageApproved: true, + ); + + Future _onRejectNameApproveImage( + RejectNameApproveImage event, + _Emit emit, + ) => _moderateOrder( + orderId: event.orderId, + isNameApproved: false, + isImageApproved: true, + ); + + Future _onApproveNameRejectImage( + ApproveNameRejectImage event, + _Emit emit, + ) => _moderateOrder( + orderId: event.orderId, + isNameApproved: true, + isImageApproved: false, + ); + + Future _onRejectNameAndImage( + RejectNameAndImage event, + _Emit emit, + ) => _moderateOrder( + orderId: event.orderId, + isNameApproved: false, + isImageApproved: false, + ); + + Future _moderateOrder({ + required String orderId, + required bool isNameApproved, + required bool isImageApproved, + }) async { + final latte = state.moderationQueue.firstWhereOrNull( + (l) => l.order.id == orderId, + ); + + if (latte == null) { + _logger.warning( + 'Could not find latte for order id $orderId. ' + 'Maybe another barista moderated it at the same time?', + ); + return; + } + + final metadata = latte.metadata.copyWith( + isNameApproved: isNameApproved, + isImageApproved: isImageApproved, + status: .validated, + ); + + // No need to capture the returned metadata, since it will get picked up + // by our open `watch` streams. + await _metadataRepository.setItem(metadata); + } + + Future _onCompleteOrder(CompleteOrder event, _Emit emit) async { + final latte = state.moderationQueue.firstWhereOrNull( + (l) => l.order.id == event.orderId, + ); + + if (latte == null) { + _logger.warning( + 'Could not find latte for order id ${event.orderId}. ' + 'Maybe another moderator or barista completed it at the same time?', + ); + return; + } + + /// Optimistic update to immediately remove the order from the UI. + emit( + state.copyWith( + moderationQueue: state.moderationQueue + .where((l) => l.order.id != event.orderId) + .toList(), + ), + ); + + await _ordersRepository.completeOrder( + orderId: latte.order.id!, + // Since moderators don't have a "persona" and thus don't have a + // Barista record, we'll just use a special string to indicate that + // a moderator completed the order. + baristaId: 'moderator', + ); + } + + @override + Future close() { + _moderateStreamSub.cancel().ignore(); + _baristaStreamSub.cancel().ignore(); + return super.close(); + } +} + +/// Actions that can be taken on the ModeratorHome page. +@Freezed() +sealed class ModeratorHomeEvent with _$ModeratorHomeEvent { + /// A new list of Baristas has arrived from the server. + const factory ModeratorHomeEvent.newBaristas(List baristas) = + NewBaristas; + + /// A new list of ready-to-moderate orders has arrived from the server. + const factory ModeratorHomeEvent.newModerateQueueOrders( + List metadatas, + ) = NewModerateQueueOrders; + + /// The barista has approved both the name and image for an order. + const factory ModeratorHomeEvent.approveNameAndImage(String orderId) = + ApproveNameAndImage; + + // /// The barista has rejected the name for an order. + const factory ModeratorHomeEvent.rejectNameApproveImage(String orderId) = + RejectNameApproveImage; + + // /// The barista has rejected the name for an order. + const factory ModeratorHomeEvent.approveNameRejectImage(String orderId) = + ApproveNameRejectImage; + + // /// The barista has rejected both the name and image for an order. + const factory ModeratorHomeEvent.rejectNameAndImage(String orderId) = + RejectNameAndImage; + + /// The moderator has completed an order. + const factory ModeratorHomeEvent.completeOrder(String orderId) = CompleteOrder; +} + +/// {@template ModeratorHomeState} +/// Complete representation of the ModeratorHome page's state. +/// {@endtemplate +@Freezed() +sealed class ModeratorHomeState with _$ModeratorHomeState { + /// {@macro ModeratorHomeState} + const factory ModeratorHomeState({ + required List moderationQueue, + @Default({}) Map baristas, + }) = _ModeratorHomeState; + const ModeratorHomeState._(); + + /// Starter state fed to the [ModeratorHomeBloc]. + factory ModeratorHomeState.initial({ + List? moderationQueue, + }) => ModeratorHomeState( + moderationQueue: moderationQueue ?? [], + ); +} + +extension _MyList on List { + /// Returns the first matching element, or null if there are no matches.` + E? firstWhereOrNull(bool Function(E element) test, {E? Function()? orElse}) { + for (final E element in this) { + if (test(element)) return element; + } + if (orElse != null) return orElse(); + return null; + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home_bloc.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home_bloc.freezed.dart new file mode 100644 index 0000000..3105bd5 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home_bloc.freezed.dart @@ -0,0 +1,948 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'moderator_home_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ModeratorHomeEvent { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ModeratorHomeEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'ModeratorHomeEvent()'; +} + + +} + +/// @nodoc +class $ModeratorHomeEventCopyWith<$Res> { +$ModeratorHomeEventCopyWith(ModeratorHomeEvent _, $Res Function(ModeratorHomeEvent) __); +} + + +/// Adds pattern-matching-related methods to [ModeratorHomeEvent]. +extension ModeratorHomeEventPatterns on ModeratorHomeEvent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( NewBaristas value)? newBaristas,TResult Function( NewModerateQueueOrders value)? newModerateQueueOrders,TResult Function( ApproveNameAndImage value)? approveNameAndImage,TResult Function( RejectNameApproveImage value)? rejectNameApproveImage,TResult Function( ApproveNameRejectImage value)? approveNameRejectImage,TResult Function( RejectNameAndImage value)? rejectNameAndImage,TResult Function( CompleteOrder value)? completeOrder,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case NewBaristas() when newBaristas != null: +return newBaristas(_that);case NewModerateQueueOrders() when newModerateQueueOrders != null: +return newModerateQueueOrders(_that);case ApproveNameAndImage() when approveNameAndImage != null: +return approveNameAndImage(_that);case RejectNameApproveImage() when rejectNameApproveImage != null: +return rejectNameApproveImage(_that);case ApproveNameRejectImage() when approveNameRejectImage != null: +return approveNameRejectImage(_that);case RejectNameAndImage() when rejectNameAndImage != null: +return rejectNameAndImage(_that);case CompleteOrder() when completeOrder != null: +return completeOrder(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( NewBaristas value) newBaristas,required TResult Function( NewModerateQueueOrders value) newModerateQueueOrders,required TResult Function( ApproveNameAndImage value) approveNameAndImage,required TResult Function( RejectNameApproveImage value) rejectNameApproveImage,required TResult Function( ApproveNameRejectImage value) approveNameRejectImage,required TResult Function( RejectNameAndImage value) rejectNameAndImage,required TResult Function( CompleteOrder value) completeOrder,}){ +final _that = this; +switch (_that) { +case NewBaristas(): +return newBaristas(_that);case NewModerateQueueOrders(): +return newModerateQueueOrders(_that);case ApproveNameAndImage(): +return approveNameAndImage(_that);case RejectNameApproveImage(): +return rejectNameApproveImage(_that);case ApproveNameRejectImage(): +return approveNameRejectImage(_that);case RejectNameAndImage(): +return rejectNameAndImage(_that);case CompleteOrder(): +return completeOrder(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( NewBaristas value)? newBaristas,TResult? Function( NewModerateQueueOrders value)? newModerateQueueOrders,TResult? Function( ApproveNameAndImage value)? approveNameAndImage,TResult? Function( RejectNameApproveImage value)? rejectNameApproveImage,TResult? Function( ApproveNameRejectImage value)? approveNameRejectImage,TResult? Function( RejectNameAndImage value)? rejectNameAndImage,TResult? Function( CompleteOrder value)? completeOrder,}){ +final _that = this; +switch (_that) { +case NewBaristas() when newBaristas != null: +return newBaristas(_that);case NewModerateQueueOrders() when newModerateQueueOrders != null: +return newModerateQueueOrders(_that);case ApproveNameAndImage() when approveNameAndImage != null: +return approveNameAndImage(_that);case RejectNameApproveImage() when rejectNameApproveImage != null: +return rejectNameApproveImage(_that);case ApproveNameRejectImage() when approveNameRejectImage != null: +return approveNameRejectImage(_that);case RejectNameAndImage() when rejectNameAndImage != null: +return rejectNameAndImage(_that);case CompleteOrder() when completeOrder != null: +return completeOrder(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( List baristas)? newBaristas,TResult Function( List metadatas)? newModerateQueueOrders,TResult Function( String orderId)? approveNameAndImage,TResult Function( String orderId)? rejectNameApproveImage,TResult Function( String orderId)? approveNameRejectImage,TResult Function( String orderId)? rejectNameAndImage,TResult Function( String orderId)? completeOrder,required TResult orElse(),}) {final _that = this; +switch (_that) { +case NewBaristas() when newBaristas != null: +return newBaristas(_that.baristas);case NewModerateQueueOrders() when newModerateQueueOrders != null: +return newModerateQueueOrders(_that.metadatas);case ApproveNameAndImage() when approveNameAndImage != null: +return approveNameAndImage(_that.orderId);case RejectNameApproveImage() when rejectNameApproveImage != null: +return rejectNameApproveImage(_that.orderId);case ApproveNameRejectImage() when approveNameRejectImage != null: +return approveNameRejectImage(_that.orderId);case RejectNameAndImage() when rejectNameAndImage != null: +return rejectNameAndImage(_that.orderId);case CompleteOrder() when completeOrder != null: +return completeOrder(_that.orderId);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( List baristas) newBaristas,required TResult Function( List metadatas) newModerateQueueOrders,required TResult Function( String orderId) approveNameAndImage,required TResult Function( String orderId) rejectNameApproveImage,required TResult Function( String orderId) approveNameRejectImage,required TResult Function( String orderId) rejectNameAndImage,required TResult Function( String orderId) completeOrder,}) {final _that = this; +switch (_that) { +case NewBaristas(): +return newBaristas(_that.baristas);case NewModerateQueueOrders(): +return newModerateQueueOrders(_that.metadatas);case ApproveNameAndImage(): +return approveNameAndImage(_that.orderId);case RejectNameApproveImage(): +return rejectNameApproveImage(_that.orderId);case ApproveNameRejectImage(): +return approveNameRejectImage(_that.orderId);case RejectNameAndImage(): +return rejectNameAndImage(_that.orderId);case CompleteOrder(): +return completeOrder(_that.orderId);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( List baristas)? newBaristas,TResult? Function( List metadatas)? newModerateQueueOrders,TResult? Function( String orderId)? approveNameAndImage,TResult? Function( String orderId)? rejectNameApproveImage,TResult? Function( String orderId)? approveNameRejectImage,TResult? Function( String orderId)? rejectNameAndImage,TResult? Function( String orderId)? completeOrder,}) {final _that = this; +switch (_that) { +case NewBaristas() when newBaristas != null: +return newBaristas(_that.baristas);case NewModerateQueueOrders() when newModerateQueueOrders != null: +return newModerateQueueOrders(_that.metadatas);case ApproveNameAndImage() when approveNameAndImage != null: +return approveNameAndImage(_that.orderId);case RejectNameApproveImage() when rejectNameApproveImage != null: +return rejectNameApproveImage(_that.orderId);case ApproveNameRejectImage() when approveNameRejectImage != null: +return approveNameRejectImage(_that.orderId);case RejectNameAndImage() when rejectNameAndImage != null: +return rejectNameAndImage(_that.orderId);case CompleteOrder() when completeOrder != null: +return completeOrder(_that.orderId);case _: + return null; + +} +} + +} + +/// @nodoc + + +class NewBaristas implements ModeratorHomeEvent { + const NewBaristas(final List baristas): _baristas = baristas; + + + final List _baristas; + List get baristas { + if (_baristas is EqualUnmodifiableListView) return _baristas; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_baristas); +} + + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NewBaristasCopyWith get copyWith => _$NewBaristasCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NewBaristas&&const DeepCollectionEquality().equals(other._baristas, _baristas)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_baristas)); + +@override +String toString() { + return 'ModeratorHomeEvent.newBaristas(baristas: $baristas)'; +} + + +} + +/// @nodoc +abstract mixin class $NewBaristasCopyWith<$Res> implements $ModeratorHomeEventCopyWith<$Res> { + factory $NewBaristasCopyWith(NewBaristas value, $Res Function(NewBaristas) _then) = _$NewBaristasCopyWithImpl; +@useResult +$Res call({ + List baristas +}); + + + + +} +/// @nodoc +class _$NewBaristasCopyWithImpl<$Res> + implements $NewBaristasCopyWith<$Res> { + _$NewBaristasCopyWithImpl(this._self, this._then); + + final NewBaristas _self; + final $Res Function(NewBaristas) _then; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? baristas = null,}) { + return _then(NewBaristas( +null == baristas ? _self._baristas : baristas // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class NewModerateQueueOrders implements ModeratorHomeEvent { + const NewModerateQueueOrders(final List metadatas): _metadatas = metadatas; + + + final List _metadatas; + List get metadatas { + if (_metadatas is EqualUnmodifiableListView) return _metadatas; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_metadatas); +} + + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NewModerateQueueOrdersCopyWith get copyWith => _$NewModerateQueueOrdersCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NewModerateQueueOrders&&const DeepCollectionEquality().equals(other._metadatas, _metadatas)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_metadatas)); + +@override +String toString() { + return 'ModeratorHomeEvent.newModerateQueueOrders(metadatas: $metadatas)'; +} + + +} + +/// @nodoc +abstract mixin class $NewModerateQueueOrdersCopyWith<$Res> implements $ModeratorHomeEventCopyWith<$Res> { + factory $NewModerateQueueOrdersCopyWith(NewModerateQueueOrders value, $Res Function(NewModerateQueueOrders) _then) = _$NewModerateQueueOrdersCopyWithImpl; +@useResult +$Res call({ + List metadatas +}); + + + + +} +/// @nodoc +class _$NewModerateQueueOrdersCopyWithImpl<$Res> + implements $NewModerateQueueOrdersCopyWith<$Res> { + _$NewModerateQueueOrdersCopyWithImpl(this._self, this._then); + + final NewModerateQueueOrders _self; + final $Res Function(NewModerateQueueOrders) _then; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? metadatas = null,}) { + return _then(NewModerateQueueOrders( +null == metadatas ? _self._metadatas : metadatas // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class ApproveNameAndImage implements ModeratorHomeEvent { + const ApproveNameAndImage(this.orderId); + + + final String orderId; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ApproveNameAndImageCopyWith get copyWith => _$ApproveNameAndImageCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ApproveNameAndImage&&(identical(other.orderId, orderId) || other.orderId == orderId)); +} + + +@override +int get hashCode => Object.hash(runtimeType,orderId); + +@override +String toString() { + return 'ModeratorHomeEvent.approveNameAndImage(orderId: $orderId)'; +} + + +} + +/// @nodoc +abstract mixin class $ApproveNameAndImageCopyWith<$Res> implements $ModeratorHomeEventCopyWith<$Res> { + factory $ApproveNameAndImageCopyWith(ApproveNameAndImage value, $Res Function(ApproveNameAndImage) _then) = _$ApproveNameAndImageCopyWithImpl; +@useResult +$Res call({ + String orderId +}); + + + + +} +/// @nodoc +class _$ApproveNameAndImageCopyWithImpl<$Res> + implements $ApproveNameAndImageCopyWith<$Res> { + _$ApproveNameAndImageCopyWithImpl(this._self, this._then); + + final ApproveNameAndImage _self; + final $Res Function(ApproveNameAndImage) _then; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? orderId = null,}) { + return _then(ApproveNameAndImage( +null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class RejectNameApproveImage implements ModeratorHomeEvent { + const RejectNameApproveImage(this.orderId); + + + final String orderId; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$RejectNameApproveImageCopyWith get copyWith => _$RejectNameApproveImageCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RejectNameApproveImage&&(identical(other.orderId, orderId) || other.orderId == orderId)); +} + + +@override +int get hashCode => Object.hash(runtimeType,orderId); + +@override +String toString() { + return 'ModeratorHomeEvent.rejectNameApproveImage(orderId: $orderId)'; +} + + +} + +/// @nodoc +abstract mixin class $RejectNameApproveImageCopyWith<$Res> implements $ModeratorHomeEventCopyWith<$Res> { + factory $RejectNameApproveImageCopyWith(RejectNameApproveImage value, $Res Function(RejectNameApproveImage) _then) = _$RejectNameApproveImageCopyWithImpl; +@useResult +$Res call({ + String orderId +}); + + + + +} +/// @nodoc +class _$RejectNameApproveImageCopyWithImpl<$Res> + implements $RejectNameApproveImageCopyWith<$Res> { + _$RejectNameApproveImageCopyWithImpl(this._self, this._then); + + final RejectNameApproveImage _self; + final $Res Function(RejectNameApproveImage) _then; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? orderId = null,}) { + return _then(RejectNameApproveImage( +null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class ApproveNameRejectImage implements ModeratorHomeEvent { + const ApproveNameRejectImage(this.orderId); + + + final String orderId; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ApproveNameRejectImageCopyWith get copyWith => _$ApproveNameRejectImageCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ApproveNameRejectImage&&(identical(other.orderId, orderId) || other.orderId == orderId)); +} + + +@override +int get hashCode => Object.hash(runtimeType,orderId); + +@override +String toString() { + return 'ModeratorHomeEvent.approveNameRejectImage(orderId: $orderId)'; +} + + +} + +/// @nodoc +abstract mixin class $ApproveNameRejectImageCopyWith<$Res> implements $ModeratorHomeEventCopyWith<$Res> { + factory $ApproveNameRejectImageCopyWith(ApproveNameRejectImage value, $Res Function(ApproveNameRejectImage) _then) = _$ApproveNameRejectImageCopyWithImpl; +@useResult +$Res call({ + String orderId +}); + + + + +} +/// @nodoc +class _$ApproveNameRejectImageCopyWithImpl<$Res> + implements $ApproveNameRejectImageCopyWith<$Res> { + _$ApproveNameRejectImageCopyWithImpl(this._self, this._then); + + final ApproveNameRejectImage _self; + final $Res Function(ApproveNameRejectImage) _then; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? orderId = null,}) { + return _then(ApproveNameRejectImage( +null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class RejectNameAndImage implements ModeratorHomeEvent { + const RejectNameAndImage(this.orderId); + + + final String orderId; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$RejectNameAndImageCopyWith get copyWith => _$RejectNameAndImageCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RejectNameAndImage&&(identical(other.orderId, orderId) || other.orderId == orderId)); +} + + +@override +int get hashCode => Object.hash(runtimeType,orderId); + +@override +String toString() { + return 'ModeratorHomeEvent.rejectNameAndImage(orderId: $orderId)'; +} + + +} + +/// @nodoc +abstract mixin class $RejectNameAndImageCopyWith<$Res> implements $ModeratorHomeEventCopyWith<$Res> { + factory $RejectNameAndImageCopyWith(RejectNameAndImage value, $Res Function(RejectNameAndImage) _then) = _$RejectNameAndImageCopyWithImpl; +@useResult +$Res call({ + String orderId +}); + + + + +} +/// @nodoc +class _$RejectNameAndImageCopyWithImpl<$Res> + implements $RejectNameAndImageCopyWith<$Res> { + _$RejectNameAndImageCopyWithImpl(this._self, this._then); + + final RejectNameAndImage _self; + final $Res Function(RejectNameAndImage) _then; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? orderId = null,}) { + return _then(RejectNameAndImage( +null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class CompleteOrder implements ModeratorHomeEvent { + const CompleteOrder(this.orderId); + + + final String orderId; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CompleteOrderCopyWith get copyWith => _$CompleteOrderCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CompleteOrder&&(identical(other.orderId, orderId) || other.orderId == orderId)); +} + + +@override +int get hashCode => Object.hash(runtimeType,orderId); + +@override +String toString() { + return 'ModeratorHomeEvent.completeOrder(orderId: $orderId)'; +} + + +} + +/// @nodoc +abstract mixin class $CompleteOrderCopyWith<$Res> implements $ModeratorHomeEventCopyWith<$Res> { + factory $CompleteOrderCopyWith(CompleteOrder value, $Res Function(CompleteOrder) _then) = _$CompleteOrderCopyWithImpl; +@useResult +$Res call({ + String orderId +}); + + + + +} +/// @nodoc +class _$CompleteOrderCopyWithImpl<$Res> + implements $CompleteOrderCopyWith<$Res> { + _$CompleteOrderCopyWithImpl(this._self, this._then); + + final CompleteOrder _self; + final $Res Function(CompleteOrder) _then; + +/// Create a copy of ModeratorHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? orderId = null,}) { + return _then(CompleteOrder( +null == orderId ? _self.orderId : orderId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc +mixin _$ModeratorHomeState { + + List get moderationQueue; Map get baristas; +/// Create a copy of ModeratorHomeState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ModeratorHomeStateCopyWith get copyWith => _$ModeratorHomeStateCopyWithImpl(this as ModeratorHomeState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ModeratorHomeState&&const DeepCollectionEquality().equals(other.moderationQueue, moderationQueue)&&const DeepCollectionEquality().equals(other.baristas, baristas)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(moderationQueue),const DeepCollectionEquality().hash(baristas)); + +@override +String toString() { + return 'ModeratorHomeState(moderationQueue: $moderationQueue, baristas: $baristas)'; +} + + +} + +/// @nodoc +abstract mixin class $ModeratorHomeStateCopyWith<$Res> { + factory $ModeratorHomeStateCopyWith(ModeratorHomeState value, $Res Function(ModeratorHomeState) _then) = _$ModeratorHomeStateCopyWithImpl; +@useResult +$Res call({ + List moderationQueue, Map baristas +}); + + + + +} +/// @nodoc +class _$ModeratorHomeStateCopyWithImpl<$Res> + implements $ModeratorHomeStateCopyWith<$Res> { + _$ModeratorHomeStateCopyWithImpl(this._self, this._then); + + final ModeratorHomeState _self; + final $Res Function(ModeratorHomeState) _then; + +/// Create a copy of ModeratorHomeState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? moderationQueue = null,Object? baristas = null,}) { + return _then(_self.copyWith( +moderationQueue: null == moderationQueue ? _self.moderationQueue : moderationQueue // ignore: cast_nullable_to_non_nullable +as List,baristas: null == baristas ? _self.baristas : baristas // ignore: cast_nullable_to_non_nullable +as Map, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ModeratorHomeState]. +extension ModeratorHomeStatePatterns on ModeratorHomeState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ModeratorHomeState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ModeratorHomeState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ModeratorHomeState value) $default,){ +final _that = this; +switch (_that) { +case _ModeratorHomeState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ModeratorHomeState value)? $default,){ +final _that = this; +switch (_that) { +case _ModeratorHomeState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List moderationQueue, Map baristas)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ModeratorHomeState() when $default != null: +return $default(_that.moderationQueue,_that.baristas);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List moderationQueue, Map baristas) $default,) {final _that = this; +switch (_that) { +case _ModeratorHomeState(): +return $default(_that.moderationQueue,_that.baristas);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List moderationQueue, Map baristas)? $default,) {final _that = this; +switch (_that) { +case _ModeratorHomeState() when $default != null: +return $default(_that.moderationQueue,_that.baristas);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ModeratorHomeState extends ModeratorHomeState { + const _ModeratorHomeState({required final List moderationQueue, final Map baristas = const {}}): _moderationQueue = moderationQueue,_baristas = baristas,super._(); + + + final List _moderationQueue; +@override List get moderationQueue { + if (_moderationQueue is EqualUnmodifiableListView) return _moderationQueue; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_moderationQueue); +} + + final Map _baristas; +@override@JsonKey() Map get baristas { + if (_baristas is EqualUnmodifiableMapView) return _baristas; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_baristas); +} + + +/// Create a copy of ModeratorHomeState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ModeratorHomeStateCopyWith<_ModeratorHomeState> get copyWith => __$ModeratorHomeStateCopyWithImpl<_ModeratorHomeState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ModeratorHomeState&&const DeepCollectionEquality().equals(other._moderationQueue, _moderationQueue)&&const DeepCollectionEquality().equals(other._baristas, _baristas)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_moderationQueue),const DeepCollectionEquality().hash(_baristas)); + +@override +String toString() { + return 'ModeratorHomeState(moderationQueue: $moderationQueue, baristas: $baristas)'; +} + + +} + +/// @nodoc +abstract mixin class _$ModeratorHomeStateCopyWith<$Res> implements $ModeratorHomeStateCopyWith<$Res> { + factory _$ModeratorHomeStateCopyWith(_ModeratorHomeState value, $Res Function(_ModeratorHomeState) _then) = __$ModeratorHomeStateCopyWithImpl; +@override @useResult +$Res call({ + List moderationQueue, Map baristas +}); + + + + +} +/// @nodoc +class __$ModeratorHomeStateCopyWithImpl<$Res> + implements _$ModeratorHomeStateCopyWith<$Res> { + __$ModeratorHomeStateCopyWithImpl(this._self, this._then); + + final _ModeratorHomeState _self; + final $Res Function(_ModeratorHomeState) _then; + +/// Create a copy of ModeratorHomeState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? moderationQueue = null,Object? baristas = null,}) { + return _then(_ModeratorHomeState( +moderationQueue: null == moderationQueue ? _self._moderationQueue : moderationQueue // ignore: cast_nullable_to_non_nullable +as List,baristas: null == baristas ? _self._baristas : baristas // ignore: cast_nullable_to_non_nullable +as Map, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home_view.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home_view.dart new file mode 100644 index 0000000..4e36cfa --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/home/moderator_home_view.dart @@ -0,0 +1,106 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/core/core.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/barista/widgets/widgets.dart'; +import 'package:genlatte/src/screens/moderator/home/moderator_home.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// {@template ModeratorHomeScreen} +/// Initial ModeratorHome screen. +/// {@endtemplate} +class ModeratorHomeScreen extends StatefulWidget { + /// {@macro ModeratorHomeScreen} + const ModeratorHomeScreen({super.key}); + + @override + State createState() => _ModeratorHomeScreenState(); +} + +class _ModeratorHomeScreenState extends State { + final ModeratorHomeBloc bloc = ModeratorHomeBloc(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: bloc, + builder: (context, state) { + return Scaffold( + headers: [ + AppBar( + backgroundColor: AppColors.almostBlack, + leading: [ + IconButton.ghost( + icon: const Icon(Icons.print_rounded, color: AppColors.white), + onPressed: () => GetIt.I().toMachines(), + ), + IconButton.ghost( + icon: const Icon(Icons.edit_rounded, color: AppColors.white), + onPressed: () => GetIt.I().toOptions(), + ), + ], + title: TripleTapDetector( + semanticLabel: 'Header', + semanticHint: 'Triple tap to log out', + onPressed: () => GetIt.I().signOut(), + child: Center( + child: const Text( + 'Moderator Queue', + style: TextStyle(color: AppColors.white), + ).h3, + ), + ), + ), + ], + child: InternalOrderQueues.moderator( + baristas: state.baristas, + orders: state.moderationQueue, + onApproveAll: (orderId) async { + bloc.add(ApproveNameAndImage(orderId)); + await GetIt.I.get().logEvent( + name: 'moderator_approve_all', + ); + }, + onRejectName: (orderId) async { + bloc.add(RejectNameApproveImage(orderId)); + await GetIt.I.get().logEvent( + name: 'moderator_reject_name', + ); + }, + onRejectImage: (orderId) async { + bloc.add(ApproveNameRejectImage(orderId)); + await GetIt.I.get().logEvent( + name: 'moderator_reject_image', + ); + }, + onRejectBoth: (orderId) async { + bloc.add(RejectNameAndImage(orderId)); + await GetIt.I.get().logEvent( + name: 'moderator_reject_both', + ); + }, + onCompletePressed: (orderId) async { + bloc.add(CompleteOrder(orderId)); + await GetIt.I.get().logEvent( + name: 'moderator_complete_order', + ); + }, + ), + ); + }, + ); + } + + @override + Future dispose() async { + await bloc.close(); + super.dispose(); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/machines/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/CONTEXT.md new file mode 100644 index 0000000..559adc3 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/CONTEXT.md @@ -0,0 +1,21 @@ +# Moderator Machines + +**Purpose:** +Provides full CRUD (Create, Read, Update, Delete) capability specifically for moderating hardware/printer Machine status in the coffee shop. + +**Detailed File Overviews:** + +- `machines.dart`: + - **Description**: Barrel export file. + +- `machines_bloc.dart`: + - **Description**: State management for machine tracking. + - **Core Logic**: Subscribes to the `Repository` to watch the live list of all physical printers connected to the system. Handles creating new machines via `forceInsert: true` (since IDs act as physical printer identifiers) and exposes toggle logic (`ToggleMachineStatus`) and deletion logic. + +- `machines_view.dart`: + - **Description**: The primary UI interface for the Machines admin portal. + - **Core Logic**: Determines if the viewport implies mobile (`ListView`) vs desktop/tablet (`GridView`) formatting. Loops over the available `machines`, emitting instances of `MachineCard`. Provides a robust creation dialog encompassing fields for ID, Name, and Black/White isolation status, as well as destructive confirmation dialogs. + +**Dependencies/Relationships:** +- Renders `MachineCard` from its `widgets/` subdirectory. +- Accessed by moderators navigating from the `ModeratorHomeScreen`. diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines.dart new file mode 100644 index 0000000..6519f49 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'machines_bloc.dart'; +export 'machines_view.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines_bloc.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines_bloc.dart new file mode 100644 index 0000000..bcb2f68 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines_bloc.dart @@ -0,0 +1,106 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +part 'machines_bloc.freezed.dart'; + +typedef _Emit = Emitter; + +/// {@template MachinesBloc} +/// State manager for the Machines CRUD page. +/// {@endtemplate} +class MachinesBloc extends Bloc { + /// {@macro MachinesBloc} + MachinesBloc() : super(MachinesState.initial()) { + on( + (event, _Emit emit) => switch (event) { + NewMachines() => _onNewMachines(event, emit), + ToggleMachineStatus() => _onToggleMachineStatus(event, emit), + CreateMachine() => _onCreateMachine(event, emit), + DeleteMachine() => _onDeleteMachine(event, emit), + }, + ); + + _machinesRepository = GetIt.I>(); + + _machinesStreamSub = _machinesRepository.watchList().listen( + (machines) => add(NewMachines(machines)), + ); + } + + late final Repository _machinesRepository; + + late final StreamSubscription> _machinesStreamSub; + + void _onNewMachines(NewMachines event, _Emit emit) => + emit(state.copyWith(machines: event.machines)); + + Future _onToggleMachineStatus( + ToggleMachineStatus event, + _Emit emit, + ) async { + await _machinesRepository.setItem( + event.machine.copyWith(isActive: !event.machine.isActive), + ); + } + + Future _onCreateMachine(CreateMachine event, _Emit emit) async => + // Ids are set by the physical printers, so the Moderators must supply + // those instead of letting Firestore set typical document Ids. + // Therefore, we must use `forceInsert: true`. + _machinesRepository.setItem( + event.machine, + RequestDetails.write(forceInsert: true), + ); + + Future _onDeleteMachine(DeleteMachine event, _Emit emit) async => + _machinesRepository.delete(event.id); + + @override + Future close() { + _machinesStreamSub.cancel().ignore(); + return super.close(); + } +} + +/// Actions that can be taken on the Machines page. +@Freezed() +sealed class MachinesEvent with _$MachinesEvent { + /// A new list of machines has arrived from the server. + const factory MachinesEvent.newMachines(List machines) = NewMachines; + + /// Toggles the active status of the given machine. + const factory MachinesEvent.toggleMachineStatus( + Machine machine, + ) = ToggleMachineStatus; + + /// Creates a new machine with the given properties. + const factory MachinesEvent.createMachine(Machine machine) = CreateMachine; + + /// Deletes the machine with the given id. + const factory MachinesEvent.deleteMachine(String id) = DeleteMachine; +} + +/// {@template MachinesState} +/// Complete representation of the Machines page's state. +/// {@endtemplate +@Freezed() +sealed class MachinesState with _$MachinesState { + /// {@macro MachinesState} + const factory MachinesState({ + /// All machines available on the server. + @Default([]) List machines, + }) = _MachinesState; + const MachinesState._(); + + /// Starter state fed to the [MachinesBloc]. + factory MachinesState.initial() => const MachinesState(); +} diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines_bloc.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines_bloc.freezed.dart new file mode 100644 index 0000000..ebe40e8 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines_bloc.freezed.dart @@ -0,0 +1,738 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'machines_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$MachinesEvent { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is MachinesEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'MachinesEvent()'; +} + + +} + +/// @nodoc +class $MachinesEventCopyWith<$Res> { +$MachinesEventCopyWith(MachinesEvent _, $Res Function(MachinesEvent) __); +} + + +/// Adds pattern-matching-related methods to [MachinesEvent]. +extension MachinesEventPatterns on MachinesEvent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( NewMachines value)? newMachines,TResult Function( ToggleMachineStatus value)? toggleMachineStatus,TResult Function( CreateMachine value)? createMachine,TResult Function( DeleteMachine value)? deleteMachine,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case NewMachines() when newMachines != null: +return newMachines(_that);case ToggleMachineStatus() when toggleMachineStatus != null: +return toggleMachineStatus(_that);case CreateMachine() when createMachine != null: +return createMachine(_that);case DeleteMachine() when deleteMachine != null: +return deleteMachine(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( NewMachines value) newMachines,required TResult Function( ToggleMachineStatus value) toggleMachineStatus,required TResult Function( CreateMachine value) createMachine,required TResult Function( DeleteMachine value) deleteMachine,}){ +final _that = this; +switch (_that) { +case NewMachines(): +return newMachines(_that);case ToggleMachineStatus(): +return toggleMachineStatus(_that);case CreateMachine(): +return createMachine(_that);case DeleteMachine(): +return deleteMachine(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( NewMachines value)? newMachines,TResult? Function( ToggleMachineStatus value)? toggleMachineStatus,TResult? Function( CreateMachine value)? createMachine,TResult? Function( DeleteMachine value)? deleteMachine,}){ +final _that = this; +switch (_that) { +case NewMachines() when newMachines != null: +return newMachines(_that);case ToggleMachineStatus() when toggleMachineStatus != null: +return toggleMachineStatus(_that);case CreateMachine() when createMachine != null: +return createMachine(_that);case DeleteMachine() when deleteMachine != null: +return deleteMachine(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( List machines)? newMachines,TResult Function( Machine machine)? toggleMachineStatus,TResult Function( Machine machine)? createMachine,TResult Function( String id)? deleteMachine,required TResult orElse(),}) {final _that = this; +switch (_that) { +case NewMachines() when newMachines != null: +return newMachines(_that.machines);case ToggleMachineStatus() when toggleMachineStatus != null: +return toggleMachineStatus(_that.machine);case CreateMachine() when createMachine != null: +return createMachine(_that.machine);case DeleteMachine() when deleteMachine != null: +return deleteMachine(_that.id);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( List machines) newMachines,required TResult Function( Machine machine) toggleMachineStatus,required TResult Function( Machine machine) createMachine,required TResult Function( String id) deleteMachine,}) {final _that = this; +switch (_that) { +case NewMachines(): +return newMachines(_that.machines);case ToggleMachineStatus(): +return toggleMachineStatus(_that.machine);case CreateMachine(): +return createMachine(_that.machine);case DeleteMachine(): +return deleteMachine(_that.id);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( List machines)? newMachines,TResult? Function( Machine machine)? toggleMachineStatus,TResult? Function( Machine machine)? createMachine,TResult? Function( String id)? deleteMachine,}) {final _that = this; +switch (_that) { +case NewMachines() when newMachines != null: +return newMachines(_that.machines);case ToggleMachineStatus() when toggleMachineStatus != null: +return toggleMachineStatus(_that.machine);case CreateMachine() when createMachine != null: +return createMachine(_that.machine);case DeleteMachine() when deleteMachine != null: +return deleteMachine(_that.id);case _: + return null; + +} +} + +} + +/// @nodoc + + +class NewMachines implements MachinesEvent { + const NewMachines(final List machines): _machines = machines; + + + final List _machines; + List get machines { + if (_machines is EqualUnmodifiableListView) return _machines; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_machines); +} + + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NewMachinesCopyWith get copyWith => _$NewMachinesCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NewMachines&&const DeepCollectionEquality().equals(other._machines, _machines)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_machines)); + +@override +String toString() { + return 'MachinesEvent.newMachines(machines: $machines)'; +} + + +} + +/// @nodoc +abstract mixin class $NewMachinesCopyWith<$Res> implements $MachinesEventCopyWith<$Res> { + factory $NewMachinesCopyWith(NewMachines value, $Res Function(NewMachines) _then) = _$NewMachinesCopyWithImpl; +@useResult +$Res call({ + List machines +}); + + + + +} +/// @nodoc +class _$NewMachinesCopyWithImpl<$Res> + implements $NewMachinesCopyWith<$Res> { + _$NewMachinesCopyWithImpl(this._self, this._then); + + final NewMachines _self; + final $Res Function(NewMachines) _then; + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? machines = null,}) { + return _then(NewMachines( +null == machines ? _self._machines : machines // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class ToggleMachineStatus implements MachinesEvent { + const ToggleMachineStatus(this.machine); + + + final Machine machine; + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ToggleMachineStatusCopyWith get copyWith => _$ToggleMachineStatusCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ToggleMachineStatus&&(identical(other.machine, machine) || other.machine == machine)); +} + + +@override +int get hashCode => Object.hash(runtimeType,machine); + +@override +String toString() { + return 'MachinesEvent.toggleMachineStatus(machine: $machine)'; +} + + +} + +/// @nodoc +abstract mixin class $ToggleMachineStatusCopyWith<$Res> implements $MachinesEventCopyWith<$Res> { + factory $ToggleMachineStatusCopyWith(ToggleMachineStatus value, $Res Function(ToggleMachineStatus) _then) = _$ToggleMachineStatusCopyWithImpl; +@useResult +$Res call({ + Machine machine +}); + + +$MachineCopyWith<$Res> get machine; + +} +/// @nodoc +class _$ToggleMachineStatusCopyWithImpl<$Res> + implements $ToggleMachineStatusCopyWith<$Res> { + _$ToggleMachineStatusCopyWithImpl(this._self, this._then); + + final ToggleMachineStatus _self; + final $Res Function(ToggleMachineStatus) _then; + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? machine = null,}) { + return _then(ToggleMachineStatus( +null == machine ? _self.machine : machine // ignore: cast_nullable_to_non_nullable +as Machine, + )); +} + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MachineCopyWith<$Res> get machine { + + return $MachineCopyWith<$Res>(_self.machine, (value) { + return _then(_self.copyWith(machine: value)); + }); +} +} + +/// @nodoc + + +class CreateMachine implements MachinesEvent { + const CreateMachine(this.machine); + + + final Machine machine; + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CreateMachineCopyWith get copyWith => _$CreateMachineCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CreateMachine&&(identical(other.machine, machine) || other.machine == machine)); +} + + +@override +int get hashCode => Object.hash(runtimeType,machine); + +@override +String toString() { + return 'MachinesEvent.createMachine(machine: $machine)'; +} + + +} + +/// @nodoc +abstract mixin class $CreateMachineCopyWith<$Res> implements $MachinesEventCopyWith<$Res> { + factory $CreateMachineCopyWith(CreateMachine value, $Res Function(CreateMachine) _then) = _$CreateMachineCopyWithImpl; +@useResult +$Res call({ + Machine machine +}); + + +$MachineCopyWith<$Res> get machine; + +} +/// @nodoc +class _$CreateMachineCopyWithImpl<$Res> + implements $CreateMachineCopyWith<$Res> { + _$CreateMachineCopyWithImpl(this._self, this._then); + + final CreateMachine _self; + final $Res Function(CreateMachine) _then; + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? machine = null,}) { + return _then(CreateMachine( +null == machine ? _self.machine : machine // ignore: cast_nullable_to_non_nullable +as Machine, + )); +} + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MachineCopyWith<$Res> get machine { + + return $MachineCopyWith<$Res>(_self.machine, (value) { + return _then(_self.copyWith(machine: value)); + }); +} +} + +/// @nodoc + + +class DeleteMachine implements MachinesEvent { + const DeleteMachine(this.id); + + + final String id; + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DeleteMachineCopyWith get copyWith => _$DeleteMachineCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DeleteMachine&&(identical(other.id, id) || other.id == id)); +} + + +@override +int get hashCode => Object.hash(runtimeType,id); + +@override +String toString() { + return 'MachinesEvent.deleteMachine(id: $id)'; +} + + +} + +/// @nodoc +abstract mixin class $DeleteMachineCopyWith<$Res> implements $MachinesEventCopyWith<$Res> { + factory $DeleteMachineCopyWith(DeleteMachine value, $Res Function(DeleteMachine) _then) = _$DeleteMachineCopyWithImpl; +@useResult +$Res call({ + String id +}); + + + + +} +/// @nodoc +class _$DeleteMachineCopyWithImpl<$Res> + implements $DeleteMachineCopyWith<$Res> { + _$DeleteMachineCopyWithImpl(this._self, this._then); + + final DeleteMachine _self; + final $Res Function(DeleteMachine) _then; + +/// Create a copy of MachinesEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? id = null,}) { + return _then(DeleteMachine( +null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc +mixin _$MachinesState { + +/// All machines available on the server. + List get machines; +/// Create a copy of MachinesState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$MachinesStateCopyWith get copyWith => _$MachinesStateCopyWithImpl(this as MachinesState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is MachinesState&&const DeepCollectionEquality().equals(other.machines, machines)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(machines)); + +@override +String toString() { + return 'MachinesState(machines: $machines)'; +} + + +} + +/// @nodoc +abstract mixin class $MachinesStateCopyWith<$Res> { + factory $MachinesStateCopyWith(MachinesState value, $Res Function(MachinesState) _then) = _$MachinesStateCopyWithImpl; +@useResult +$Res call({ + List machines +}); + + + + +} +/// @nodoc +class _$MachinesStateCopyWithImpl<$Res> + implements $MachinesStateCopyWith<$Res> { + _$MachinesStateCopyWithImpl(this._self, this._then); + + final MachinesState _self; + final $Res Function(MachinesState) _then; + +/// Create a copy of MachinesState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? machines = null,}) { + return _then(_self.copyWith( +machines: null == machines ? _self.machines : machines // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [MachinesState]. +extension MachinesStatePatterns on MachinesState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _MachinesState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _MachinesState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _MachinesState value) $default,){ +final _that = this; +switch (_that) { +case _MachinesState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _MachinesState value)? $default,){ +final _that = this; +switch (_that) { +case _MachinesState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List machines)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _MachinesState() when $default != null: +return $default(_that.machines);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List machines) $default,) {final _that = this; +switch (_that) { +case _MachinesState(): +return $default(_that.machines);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List machines)? $default,) {final _that = this; +switch (_that) { +case _MachinesState() when $default != null: +return $default(_that.machines);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _MachinesState extends MachinesState { + const _MachinesState({final List machines = const []}): _machines = machines,super._(); + + +/// All machines available on the server. + final List _machines; +/// All machines available on the server. +@override@JsonKey() List get machines { + if (_machines is EqualUnmodifiableListView) return _machines; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_machines); +} + + +/// Create a copy of MachinesState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$MachinesStateCopyWith<_MachinesState> get copyWith => __$MachinesStateCopyWithImpl<_MachinesState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MachinesState&&const DeepCollectionEquality().equals(other._machines, _machines)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_machines)); + +@override +String toString() { + return 'MachinesState(machines: $machines)'; +} + + +} + +/// @nodoc +abstract mixin class _$MachinesStateCopyWith<$Res> implements $MachinesStateCopyWith<$Res> { + factory _$MachinesStateCopyWith(_MachinesState value, $Res Function(_MachinesState) _then) = __$MachinesStateCopyWithImpl; +@override @useResult +$Res call({ + List machines +}); + + + + +} +/// @nodoc +class __$MachinesStateCopyWithImpl<$Res> + implements _$MachinesStateCopyWith<$Res> { + __$MachinesStateCopyWithImpl(this._self, this._then); + + final _MachinesState _self; + final $Res Function(_MachinesState) _then; + +/// Create a copy of MachinesState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? machines = null,}) { + return _then(_MachinesState( +machines: null == machines ? _self._machines : machines // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines_view.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines_view.dart new file mode 100644 index 0000000..b91e141 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/machines_view.dart @@ -0,0 +1,251 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/core/core.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/moderator/machines/machines.dart'; +import 'package:genlatte/src/screens/moderator/machines/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// {@template MachinesScreen} +/// Initial Machines screen. +/// {@endtemplate} +class MachinesScreen extends StatefulWidget { + /// {@macro MachinesScreen} + const MachinesScreen({super.key}); + + @override + State createState() => _MachinesScreenState(); +} + +class _MachinesScreenState extends State { + final MachinesBloc bloc = MachinesBloc(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: bloc, + builder: (context, state) { + return Scaffold( + headers: [ + AppBar( + backgroundColor: AppColors.almostBlack, + leading: [ + IconButton.ghost( + icon: const Icon( + Icons.arrow_back_rounded, + color: AppColors.white, + ), + onPressed: () => GetIt.I().router.pop(), + ), + ], + title: Center( + child: const Text( + 'Machines', + style: TextStyle(color: AppColors.white), + ).h3, + ), + trailing: [ + IconButton.ghost( + icon: const Icon(Icons.add_rounded, color: AppColors.white), + onPressed: () => _showCreateMachineDialog(context), + ), + ], + ), + ], + child: LayoutBuilder( + builder: (context, constraints) { + if (state.machines.isEmpty) { + return const Center( + child: Text( + 'No machines available.', + style: TextStyle(color: AppColors.white), + ), + ); + } + + if (constraints.maxWidth > 800) { + return GridView.builder( + padding: const EdgeInsets.all(24), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + crossAxisSpacing: 24, + mainAxisSpacing: 24, + mainAxisExtent: 170, + ), + itemCount: state.machines.length, + itemBuilder: (context, index) { + final machine = state.machines[index]; + return MachineCard( + machine: machine, + toggleStatus: () => + bloc.add(ToggleMachineStatus(machine)), + onDelete: () => _confirmDelete(context, machine), + ); + }, + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: state.machines.length, + itemBuilder: (context, index) { + final machine = state.machines[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: MachineCard( + machine: machine, + toggleStatus: () => + bloc.add(ToggleMachineStatus(machine)), + onDelete: () => _confirmDelete(context, machine), + ), + ); + }, + ); + }, + ), + ); + }, + ); + } + + void _showCreateMachineDialog(BuildContext context) { + final nameController = TextEditingController(); + final idController = TextEditingController(); + bool isBlackAndWhite = true; + + showDialog( + context: context, + builder: (context) { + return Theme( + data: formsTheme, + child: StatefulBuilder( + builder: (context, setState) { + final size = MediaQuery.sizeOf(context); + return SizedBox( + width: min(640, size.width * 0.8), + child: AlertDialog( + title: const Text('Add New Machine'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Machine Id', + style: TextStyle(color: AppColors.almostBlack), + ).bold, + const SizedBox(height: 8), + TextField( + controller: idController, + placeholder: const Text('Machine Id'), + ), + const SizedBox(height: 24), + const Text( + 'Machine Name', + style: TextStyle(color: AppColors.almostBlack), + ).bold, + const SizedBox(height: 8), + TextField( + controller: nameController, + placeholder: const Text('Machine Name'), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Black and White Only', + style: TextStyle(color: AppColors.almostBlack), + ).bold, + Switch( + value: isBlackAndWhite, + onChanged: (val) => + setState(() => isBlackAndWhite = val), + ), + ], + ), + ], + ), + actions: [ + SecondaryButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + PrimaryButton( + onPressed: () { + final text = nameController.text.trim(); + final id = idController.text.trim(); + if (text.isNotEmpty) { + bloc.add( + CreateMachine( + Machine( + id: id, + name: text, + isBlackAndWhite: isBlackAndWhite, + ), + ), + ); + Navigator.of(context).pop(); + } + }, + child: const Text('Create'), + ), + ], + ), + ); + }, + ), + ); + }, + ).ignore(); + } + + void _confirmDelete(BuildContext context, Machine machine) { + showDialog( + context: context, + builder: (context) { + final size = MediaQuery.sizeOf(context); + return SizedBox( + width: min(640, size.width * 0.8), + child: AlertDialog( + title: const Text('Confirm Deletion'), + content: Text( + 'Are you sure you want to completely remove the machine ' + '"${machine.name}"?\n\nIf you live to regret this decision, ' + 'you will have to recreate the machine from scratch.', + style: const TextStyle( + color: AppColors.almostBlack, + fontSize: 16, + ), + ), + actions: [ + SecondaryButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + DestructiveButton( + onPressed: () { + bloc.add(DeleteMachine(machine.id)); + Navigator.of(context).pop(); + }, + child: const Text('Delete'), + ), + ], + ), + ); + }, + ).ignore(); + } + + @override + Future dispose() async { + super.dispose(); + await bloc.close(); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/machines/widgets/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/widgets/CONTEXT.md new file mode 100644 index 0000000..a345e34 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/widgets/CONTEXT.md @@ -0,0 +1,19 @@ +# Moderator Machines Widgets + +**Purpose:** +Provides common, reusable widgets for the moderator machines management interface. These widgets are responsible for visually rendering machine status and capabilities. + +**Detailed File Overviews:** + +- `machine_card.dart`: + - **Description**: Defines `MachineCard`, a stateful widget that elegantly displays an individual `Machine`'s status and capabilities in an interactive card layout. + - **Core Logic**: Manages hover animations and determines specific badges based on whether the machine is `.isActive` and `!isBlackAndWhite`. Displays status identifiers (Active/Offline) and specific capabilities (Color/B&W). + - **Exposed Methods & Usage**: Receives `ToggleStatus` and `OnDelete` callbacks to emit state changes up to the parent list or bloc. + +- `widgets.dart`: + - **Description**: Barrel file for exporting `machine_card.dart`. + - **Core Logic/Usage**: Simplifies imports for consumers of the machine widgets. + +**Dependencies/Relationships:** +- Interfaces directly with `models.dart` from `genlatte_data` to consume the `Machine` data model. +- Designed to be used by the parent `moderator/machines` screens for rendering lists of printers/machines. diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/machines/widgets/machine_card.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/widgets/machine_card.dart new file mode 100644 index 0000000..0fe2d54 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/widgets/machine_card.dart @@ -0,0 +1,214 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte_data/models.dart'; + +/// A card widget for displaying a [Machine]. +class MachineCard extends StatefulWidget { + /// Creates a new [MachineCard]. + const MachineCard({ + required this.machine, + required this.toggleStatus, + required this.onDelete, + super.key, + }); + + /// The [Machine] to display. + final Machine machine; + + /// Callback for when the toggle status button is pressed. + final void Function() toggleStatus; + + /// Callback for when the delete button is pressed. + final void Function() onDelete; + + @override + State createState() => _MachineCardState(); +} + +class _MachineCardState extends State { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + final isActive = widget.machine.isActive; + final isColor = !widget.machine.isBlackAndWhite; + + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + transform: Matrix4.translationValues(0, _isHovered ? -2.0 : 0.0, 0), + width: 320, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE0E0E0)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: _isHovered ? 0.08 : 0.05), + blurRadius: _isHovered ? 16 : 12, + offset: Offset(0, _isHovered ? 6 : 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header: Name and Status + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + widget.machine.name, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFF1F2937), + ), + ), + ), + _buildStatusBadge(isActive), + ], + ), + const SizedBox(height: 16), + + // Capabilities / Specs + _buildSpecBadge(isColor), + const SizedBox(height: 16), + + // Action Buttons + Row( + children: [ + Expanded( + child: _buildActionButton( + label: isActive ? 'Deactivate' : 'Activate', + textColor: isActive + ? const Color.fromARGB(255, 223, 93, 93) + : const Color(0xFF2563EB), + backgroundColor: isActive + ? AppColors.white + : const Color(0xFFEFF6FF), + borderColor: isActive + ? const Color.fromARGB(255, 255, 202, 202) + : const Color(0xFFBFDBFE), + onPressed: widget.toggleStatus, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildActionButton( + label: 'Delete', + textColor: const Color(0xFFDC2626), + backgroundColor: const Color(0xFFFEF2F2), + borderColor: const Color(0xFFFECACA), + onPressed: widget.onDelete, + ), + ), + ], + ), + ], + ), + ), + ); + } + + // --- Helper Widgets for Internal Components --- + + Widget _buildStatusBadge(bool isActive) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: isActive ? const Color(0xFFDCFCE7) : const Color(0xFFFEE2E2), + borderRadius: BorderRadius.circular(9999), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: isActive + ? const Color(0xFF22C55E) + : const Color(0xFFEF4444), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + isActive ? 'ACTIVE' : 'OFFLINE', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + color: isActive + ? const Color(0xFF15803D) + : const Color(0xFFB91C1C), + ), + ), + ], + ), + ); + } + + Widget _buildSpecBadge(bool isColor) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF3F4F6), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + isColor ? '🎨 Color' : '⚫ B&W Only', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF4B5563), + ), + ), + ], + ), + ); + } + + Widget _buildActionButton({ + required String label, + required Color textColor, + required Color backgroundColor, + required Color borderColor, + required VoidCallback onPressed, + }) { + return TextButton( + onPressed: onPressed, + style: TextButton.styleFrom( + foregroundColor: textColor, + backgroundColor: backgroundColor, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: borderColor), + ), + ), + child: Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/machines/widgets/widgets.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/widgets/widgets.dart new file mode 100644 index 0000000..9149f2f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/machines/widgets/widgets.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'machine_card.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/moderator.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/moderator.dart new file mode 100644 index 0000000..172d072 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/moderator.dart @@ -0,0 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'home/moderator_home.dart'; +export 'machines/machines.dart'; +export 'options/options.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/options/options.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/options/options.dart new file mode 100644 index 0000000..d85991d --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/options/options.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'options_bloc.dart'; +export 'options_view.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/options/options_bloc.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/options/options_bloc.dart new file mode 100644 index 0000000..41c94ef --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/options/options_bloc.dart @@ -0,0 +1,123 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +part 'options_bloc.freezed.dart'; + +typedef _Emit = Emitter; + +/// {@template OptionsBloc} +/// State manager for the Options CRUD page. +/// {@endtemplate} +class OptionsBloc extends Bloc { + /// {@macro OptionsBloc} + OptionsBloc() : super(OptionsState.initial()) { + on( + (event, _Emit emit) => switch (event) { + NewOptions() => _onNewOptions(event, emit), + CreateOption() => _onCreateOption(event, emit), + ToggleOptionStatus() => _onToggleOptionStatus(event, emit), + DeleteOption() => _onDeleteOption(event, emit), + }, + ); + + _optionsRepository = GetIt.I>(); + + _optionsStreamSub = _optionsRepository.watchList().listen((options) { + add(NewOptions(options)); + }); + } + + late final Repository _optionsRepository; + late final StreamSubscription> _optionsStreamSub; + + void _onNewOptions(NewOptions event, _Emit emit) => + emit(state.copyWith(options: event.options)); + + Future _onCreateOption(CreateOption event, _Emit emit) async { + final newValues = [...event.parent.values, LatteOption(name: event.name)]; + await _optionsRepository.setItem( + event.parent.copyWith(values: newValues), + ); + } + + Future _onToggleOptionStatus( + ToggleOptionStatus event, + _Emit emit, + ) async { + final newValues = List.from(event.parent.values); + final index = newValues.indexOf(event.option); + if (index != -1) { + newValues[index] = event.option.copyWith( + isAvailable: !event.option.isAvailable, + ); + await _optionsRepository.setItem( + event.parent.copyWith(values: newValues), + ); + } + } + + Future _onDeleteOption(DeleteOption event, _Emit emit) async { + final newValues = List.from(event.parent.values) + ..remove(event.option); + await _optionsRepository.setItem( + event.parent.copyWith(values: newValues), + ); + } + + @override + Future close() { + _optionsStreamSub.cancel().ignore(); + return super.close(); + } +} + +/// Actions that can be taken on the Options page. +@Freezed() +sealed class OptionsEvent with _$OptionsEvent { + /// A new list of options has arrived from the server. + const factory OptionsEvent.newOptions(List options) = + NewOptions; + + /// Creates a new [LatteOption] within the given [LatteOptions]. + const factory OptionsEvent.createOption( + LatteOptions parent, + String name, + ) = CreateOption; + + /// Toggles the active status of the given option. + const factory OptionsEvent.toggleOptionStatus( + LatteOptions parent, + LatteOption option, + ) = ToggleOptionStatus; + + /// Deletes the given [LatteOption] from the given [LatteOptions]. + const factory OptionsEvent.deleteOption( + LatteOptions parent, + LatteOption option, + ) = DeleteOption; +} + +/// {@template OptionsState} +/// Complete representation of the Options page's state. +/// {@endtemplate +@Freezed() +sealed class OptionsState with _$OptionsState { + /// {@macro OptionsState} + const factory OptionsState({ + /// All options available on the server. + @Default([]) List options, + }) = _OptionsState; + const OptionsState._(); + + /// Starter state fed to the [OptionsBloc]. + factory OptionsState.initial() => const OptionsState(); +} diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/options/options_bloc.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/options/options_bloc.freezed.dart new file mode 100644 index 0000000..68e614f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/options/options_bloc.freezed.dart @@ -0,0 +1,771 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'options_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$OptionsEvent { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is OptionsEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'OptionsEvent()'; +} + + +} + +/// @nodoc +class $OptionsEventCopyWith<$Res> { +$OptionsEventCopyWith(OptionsEvent _, $Res Function(OptionsEvent) __); +} + + +/// Adds pattern-matching-related methods to [OptionsEvent]. +extension OptionsEventPatterns on OptionsEvent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( NewOptions value)? newOptions,TResult Function( CreateOption value)? createOption,TResult Function( ToggleOptionStatus value)? toggleOptionStatus,TResult Function( DeleteOption value)? deleteOption,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case NewOptions() when newOptions != null: +return newOptions(_that);case CreateOption() when createOption != null: +return createOption(_that);case ToggleOptionStatus() when toggleOptionStatus != null: +return toggleOptionStatus(_that);case DeleteOption() when deleteOption != null: +return deleteOption(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( NewOptions value) newOptions,required TResult Function( CreateOption value) createOption,required TResult Function( ToggleOptionStatus value) toggleOptionStatus,required TResult Function( DeleteOption value) deleteOption,}){ +final _that = this; +switch (_that) { +case NewOptions(): +return newOptions(_that);case CreateOption(): +return createOption(_that);case ToggleOptionStatus(): +return toggleOptionStatus(_that);case DeleteOption(): +return deleteOption(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( NewOptions value)? newOptions,TResult? Function( CreateOption value)? createOption,TResult? Function( ToggleOptionStatus value)? toggleOptionStatus,TResult? Function( DeleteOption value)? deleteOption,}){ +final _that = this; +switch (_that) { +case NewOptions() when newOptions != null: +return newOptions(_that);case CreateOption() when createOption != null: +return createOption(_that);case ToggleOptionStatus() when toggleOptionStatus != null: +return toggleOptionStatus(_that);case DeleteOption() when deleteOption != null: +return deleteOption(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( List options)? newOptions,TResult Function( LatteOptions parent, String name)? createOption,TResult Function( LatteOptions parent, LatteOption option)? toggleOptionStatus,TResult Function( LatteOptions parent, LatteOption option)? deleteOption,required TResult orElse(),}) {final _that = this; +switch (_that) { +case NewOptions() when newOptions != null: +return newOptions(_that.options);case CreateOption() when createOption != null: +return createOption(_that.parent,_that.name);case ToggleOptionStatus() when toggleOptionStatus != null: +return toggleOptionStatus(_that.parent,_that.option);case DeleteOption() when deleteOption != null: +return deleteOption(_that.parent,_that.option);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( List options) newOptions,required TResult Function( LatteOptions parent, String name) createOption,required TResult Function( LatteOptions parent, LatteOption option) toggleOptionStatus,required TResult Function( LatteOptions parent, LatteOption option) deleteOption,}) {final _that = this; +switch (_that) { +case NewOptions(): +return newOptions(_that.options);case CreateOption(): +return createOption(_that.parent,_that.name);case ToggleOptionStatus(): +return toggleOptionStatus(_that.parent,_that.option);case DeleteOption(): +return deleteOption(_that.parent,_that.option);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( List options)? newOptions,TResult? Function( LatteOptions parent, String name)? createOption,TResult? Function( LatteOptions parent, LatteOption option)? toggleOptionStatus,TResult? Function( LatteOptions parent, LatteOption option)? deleteOption,}) {final _that = this; +switch (_that) { +case NewOptions() when newOptions != null: +return newOptions(_that.options);case CreateOption() when createOption != null: +return createOption(_that.parent,_that.name);case ToggleOptionStatus() when toggleOptionStatus != null: +return toggleOptionStatus(_that.parent,_that.option);case DeleteOption() when deleteOption != null: +return deleteOption(_that.parent,_that.option);case _: + return null; + +} +} + +} + +/// @nodoc + + +class NewOptions implements OptionsEvent { + const NewOptions(final List options): _options = options; + + + final List _options; + List get options { + if (_options is EqualUnmodifiableListView) return _options; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_options); +} + + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NewOptionsCopyWith get copyWith => _$NewOptionsCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NewOptions&&const DeepCollectionEquality().equals(other._options, _options)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_options)); + +@override +String toString() { + return 'OptionsEvent.newOptions(options: $options)'; +} + + +} + +/// @nodoc +abstract mixin class $NewOptionsCopyWith<$Res> implements $OptionsEventCopyWith<$Res> { + factory $NewOptionsCopyWith(NewOptions value, $Res Function(NewOptions) _then) = _$NewOptionsCopyWithImpl; +@useResult +$Res call({ + List options +}); + + + + +} +/// @nodoc +class _$NewOptionsCopyWithImpl<$Res> + implements $NewOptionsCopyWith<$Res> { + _$NewOptionsCopyWithImpl(this._self, this._then); + + final NewOptions _self; + final $Res Function(NewOptions) _then; + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? options = null,}) { + return _then(NewOptions( +null == options ? _self._options : options // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class CreateOption implements OptionsEvent { + const CreateOption(this.parent, this.name); + + + final LatteOptions parent; + final String name; + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CreateOptionCopyWith get copyWith => _$CreateOptionCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CreateOption&&(identical(other.parent, parent) || other.parent == parent)&&(identical(other.name, name) || other.name == name)); +} + + +@override +int get hashCode => Object.hash(runtimeType,parent,name); + +@override +String toString() { + return 'OptionsEvent.createOption(parent: $parent, name: $name)'; +} + + +} + +/// @nodoc +abstract mixin class $CreateOptionCopyWith<$Res> implements $OptionsEventCopyWith<$Res> { + factory $CreateOptionCopyWith(CreateOption value, $Res Function(CreateOption) _then) = _$CreateOptionCopyWithImpl; +@useResult +$Res call({ + LatteOptions parent, String name +}); + + +$LatteOptionsCopyWith<$Res> get parent; + +} +/// @nodoc +class _$CreateOptionCopyWithImpl<$Res> + implements $CreateOptionCopyWith<$Res> { + _$CreateOptionCopyWithImpl(this._self, this._then); + + final CreateOption _self; + final $Res Function(CreateOption) _then; + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? parent = null,Object? name = null,}) { + return _then(CreateOption( +null == parent ? _self.parent : parent // ignore: cast_nullable_to_non_nullable +as LatteOptions,null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOptionsCopyWith<$Res> get parent { + + return $LatteOptionsCopyWith<$Res>(_self.parent, (value) { + return _then(_self.copyWith(parent: value)); + }); +} +} + +/// @nodoc + + +class ToggleOptionStatus implements OptionsEvent { + const ToggleOptionStatus(this.parent, this.option); + + + final LatteOptions parent; + final LatteOption option; + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ToggleOptionStatusCopyWith get copyWith => _$ToggleOptionStatusCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ToggleOptionStatus&&(identical(other.parent, parent) || other.parent == parent)&&(identical(other.option, option) || other.option == option)); +} + + +@override +int get hashCode => Object.hash(runtimeType,parent,option); + +@override +String toString() { + return 'OptionsEvent.toggleOptionStatus(parent: $parent, option: $option)'; +} + + +} + +/// @nodoc +abstract mixin class $ToggleOptionStatusCopyWith<$Res> implements $OptionsEventCopyWith<$Res> { + factory $ToggleOptionStatusCopyWith(ToggleOptionStatus value, $Res Function(ToggleOptionStatus) _then) = _$ToggleOptionStatusCopyWithImpl; +@useResult +$Res call({ + LatteOptions parent, LatteOption option +}); + + +$LatteOptionsCopyWith<$Res> get parent;$LatteOptionCopyWith<$Res> get option; + +} +/// @nodoc +class _$ToggleOptionStatusCopyWithImpl<$Res> + implements $ToggleOptionStatusCopyWith<$Res> { + _$ToggleOptionStatusCopyWithImpl(this._self, this._then); + + final ToggleOptionStatus _self; + final $Res Function(ToggleOptionStatus) _then; + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? parent = null,Object? option = null,}) { + return _then(ToggleOptionStatus( +null == parent ? _self.parent : parent // ignore: cast_nullable_to_non_nullable +as LatteOptions,null == option ? _self.option : option // ignore: cast_nullable_to_non_nullable +as LatteOption, + )); +} + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOptionsCopyWith<$Res> get parent { + + return $LatteOptionsCopyWith<$Res>(_self.parent, (value) { + return _then(_self.copyWith(parent: value)); + }); +}/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOptionCopyWith<$Res> get option { + + return $LatteOptionCopyWith<$Res>(_self.option, (value) { + return _then(_self.copyWith(option: value)); + }); +} +} + +/// @nodoc + + +class DeleteOption implements OptionsEvent { + const DeleteOption(this.parent, this.option); + + + final LatteOptions parent; + final LatteOption option; + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DeleteOptionCopyWith get copyWith => _$DeleteOptionCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DeleteOption&&(identical(other.parent, parent) || other.parent == parent)&&(identical(other.option, option) || other.option == option)); +} + + +@override +int get hashCode => Object.hash(runtimeType,parent,option); + +@override +String toString() { + return 'OptionsEvent.deleteOption(parent: $parent, option: $option)'; +} + + +} + +/// @nodoc +abstract mixin class $DeleteOptionCopyWith<$Res> implements $OptionsEventCopyWith<$Res> { + factory $DeleteOptionCopyWith(DeleteOption value, $Res Function(DeleteOption) _then) = _$DeleteOptionCopyWithImpl; +@useResult +$Res call({ + LatteOptions parent, LatteOption option +}); + + +$LatteOptionsCopyWith<$Res> get parent;$LatteOptionCopyWith<$Res> get option; + +} +/// @nodoc +class _$DeleteOptionCopyWithImpl<$Res> + implements $DeleteOptionCopyWith<$Res> { + _$DeleteOptionCopyWithImpl(this._self, this._then); + + final DeleteOption _self; + final $Res Function(DeleteOption) _then; + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? parent = null,Object? option = null,}) { + return _then(DeleteOption( +null == parent ? _self.parent : parent // ignore: cast_nullable_to_non_nullable +as LatteOptions,null == option ? _self.option : option // ignore: cast_nullable_to_non_nullable +as LatteOption, + )); +} + +/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOptionsCopyWith<$Res> get parent { + + return $LatteOptionsCopyWith<$Res>(_self.parent, (value) { + return _then(_self.copyWith(parent: value)); + }); +}/// Create a copy of OptionsEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$LatteOptionCopyWith<$Res> get option { + + return $LatteOptionCopyWith<$Res>(_self.option, (value) { + return _then(_self.copyWith(option: value)); + }); +} +} + +/// @nodoc +mixin _$OptionsState { + +/// All options available on the server. + List get options; +/// Create a copy of OptionsState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$OptionsStateCopyWith get copyWith => _$OptionsStateCopyWithImpl(this as OptionsState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is OptionsState&&const DeepCollectionEquality().equals(other.options, options)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(options)); + +@override +String toString() { + return 'OptionsState(options: $options)'; +} + + +} + +/// @nodoc +abstract mixin class $OptionsStateCopyWith<$Res> { + factory $OptionsStateCopyWith(OptionsState value, $Res Function(OptionsState) _then) = _$OptionsStateCopyWithImpl; +@useResult +$Res call({ + List options +}); + + + + +} +/// @nodoc +class _$OptionsStateCopyWithImpl<$Res> + implements $OptionsStateCopyWith<$Res> { + _$OptionsStateCopyWithImpl(this._self, this._then); + + final OptionsState _self; + final $Res Function(OptionsState) _then; + +/// Create a copy of OptionsState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? options = null,}) { + return _then(_self.copyWith( +options: null == options ? _self.options : options // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [OptionsState]. +extension OptionsStatePatterns on OptionsState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _OptionsState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _OptionsState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _OptionsState value) $default,){ +final _that = this; +switch (_that) { +case _OptionsState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _OptionsState value)? $default,){ +final _that = this; +switch (_that) { +case _OptionsState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List options)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _OptionsState() when $default != null: +return $default(_that.options);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List options) $default,) {final _that = this; +switch (_that) { +case _OptionsState(): +return $default(_that.options);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List options)? $default,) {final _that = this; +switch (_that) { +case _OptionsState() when $default != null: +return $default(_that.options);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _OptionsState extends OptionsState { + const _OptionsState({final List options = const []}): _options = options,super._(); + + +/// All options available on the server. + final List _options; +/// All options available on the server. +@override@JsonKey() List get options { + if (_options is EqualUnmodifiableListView) return _options; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_options); +} + + +/// Create a copy of OptionsState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$OptionsStateCopyWith<_OptionsState> get copyWith => __$OptionsStateCopyWithImpl<_OptionsState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _OptionsState&&const DeepCollectionEquality().equals(other._options, _options)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_options)); + +@override +String toString() { + return 'OptionsState(options: $options)'; +} + + +} + +/// @nodoc +abstract mixin class _$OptionsStateCopyWith<$Res> implements $OptionsStateCopyWith<$Res> { + factory _$OptionsStateCopyWith(_OptionsState value, $Res Function(_OptionsState) _then) = __$OptionsStateCopyWithImpl; +@override @useResult +$Res call({ + List options +}); + + + + +} +/// @nodoc +class __$OptionsStateCopyWithImpl<$Res> + implements _$OptionsStateCopyWith<$Res> { + __$OptionsStateCopyWithImpl(this._self, this._then); + + final _OptionsState _self; + final $Res Function(_OptionsState) _then; + +/// Create a copy of OptionsState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? options = null,}) { + return _then(_OptionsState( +options: null == options ? _self._options : options // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/moderator/options/options_view.dart b/genlatte/genlatte-ui/lib/src/screens/moderator/options/options_view.dart new file mode 100644 index 0000000..418ffa5 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/moderator/options/options_view.dart @@ -0,0 +1,248 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/core/core.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/moderator/options/options.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// {@template OptionsScreen} +/// Initial Options screen. +/// {@endtemplate} +class OptionsScreen extends StatefulWidget { + /// {@macro OptionsScreen} + const OptionsScreen({super.key}); + + @override + State createState() => _OptionsScreenState(); +} + +class _OptionsScreenState extends State { + final OptionsBloc bloc = OptionsBloc(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: bloc, + builder: (context, state) { + return Scaffold( + headers: [ + AppBar( + backgroundColor: AppColors.almostBlack, + leading: [ + IconButton.ghost( + icon: const Icon( + Icons.arrow_back_rounded, + color: AppColors.white, + ), + onPressed: () => GetIt.I().router.pop(), + ), + ], + title: Center( + child: const Text( + 'Options', + style: TextStyle(color: AppColors.white), + ).h3, + ), + ), + ], + child: LayoutBuilder( + builder: (context, constraints) { + if (state.options.isEmpty) { + return const Center( + child: Text( + 'No options available.', + style: TextStyle(color: AppColors.white), + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(24), + itemCount: state.options.length, + itemBuilder: (context, index) { + final group = state.options[index]; + return Padding( + padding: EdgeInsets.symmetric( + horizontal: constraints.maxWidth * 0.1, + vertical: 8, + ), + child: Card( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(group.id.capitalize()).h4, + IconButton.ghost( + icon: const Icon(Icons.add_rounded), + onPressed: () => _showCreateOptionDialog( + context, + group, + ), + ), + ], + ), + const SizedBox(height: 16), + if (group.values.isEmpty) + const Text('No options in this group.'), + for (final option in group.values) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(option.name), + ), + Switch( + value: option.isAvailable, + onChanged: (val) => bloc.add( + ToggleOptionStatus(group, option), + ), + ), + IconButton.ghost( + icon: const Icon( + Icons.delete_rounded, + color: Colors.red, + ), + onPressed: () => _confirmDelete( + context, + group, + option, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ); + }, + ); + } + + void _showCreateOptionDialog(BuildContext context, LatteOptions group) { + final nameController = TextEditingController(); + + showDialog( + context: context, + builder: (context) { + return Theme( + data: formsTheme, + child: StatefulBuilder( + builder: (context, setState) { + final size = MediaQuery.sizeOf(context); + return SizedBox( + width: min(640, size.width * 0.8), + child: AlertDialog( + title: Text('Add Option to ${group.id}'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Option Name', + style: TextStyle(color: AppColors.almostBlack), + ).bold, + const SizedBox(height: 8), + TextField( + controller: nameController, + placeholder: const Text('Option Name'), + ), + ], + ), + actions: [ + SecondaryButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + PrimaryButton( + onPressed: () { + final text = nameController.text.trim(); + if (text.isNotEmpty) { + bloc.add(CreateOption(group, text)); + Navigator.of(context).pop(); + } + }, + child: const Text('Create'), + ), + ], + ), + ); + }, + ), + ); + }, + ).ignore(); + } + + void _confirmDelete( + BuildContext context, + LatteOptions group, + LatteOption option, + ) { + showDialog( + context: context, + builder: (context) { + final size = MediaQuery.sizeOf(context); + return SizedBox( + width: min(640, size.width * 0.8), + child: AlertDialog( + title: const Text('Confirm Deletion'), + content: Text( + 'Are you sure you want to completely remove the option ' + '"${option.name}" from ${group.id}?', + style: const TextStyle( + color: AppColors.almostBlack, + fontSize: 16, + ), + ), + actions: [ + SecondaryButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + DestructiveButton( + onPressed: () { + bloc.add(DeleteOption(group, option)); + Navigator.of(context).pop(); + }, + child: const Text('Delete'), + ), + ], + ), + ); + }, + ).ignore(); + } + + @override + Future dispose() async { + super.dispose(); + await bloc.close(); + } +} + +extension on String { + /// Capitalizes the first letter of the string. + String capitalize() { + if (isEmpty) return this; + return '${this[0].toUpperCase()}${substring(1)}'; + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/queue/CONTEXT.md new file mode 100644 index 0000000..04df99c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/CONTEXT.md @@ -0,0 +1,11 @@ +# Kiosk / Barista / Moderator / Queue / Recent Orders Roots + +**Purpose:** +This directory acts as the architectural boundary grouping for the application's distinct personas. + +**Implementation Details:** +- **Barrel Architecture**: This level strictly exports child module directories like `home/` and `widgets/` via an `index` / `barrel` file. +- **Child Contexts**: Please navigate into `home/` or `widgets/` subdirectories to view specific BLoC architectures, View layouts, and complex logic files tailored towards each app persona. + +**Dependencies:** +- Consumed by `core/routing/routes.dart` to map physical URLs to specific screen entry points. diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/home/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/queue/home/CONTEXT.md new file mode 100644 index 0000000..1d6c228 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/home/CONTEXT.md @@ -0,0 +1,29 @@ +# Queue Home + +**Purpose:** +Hosts the large-format, passive "Queue Board" screen used to alert consumers to the status of their submitted drinks. This application is designed to be completely autonomous, reacting dynamically to distributed databases, managing paging seamlessly across varying hardware deployments without centralized communication. + +**Detailed File Overviews:** + +- `queue_home.dart`: + - **Description**: Barrel export file. + +- `queue_home_bloc.dart`: + - **Description**: Core autonomous state management for the display board. + - **Core Logic**: Extremely complex, managing both active state subscriptions and local persisted UI settings (Page Duration, Row Heights). + - Implements a pure client-side Sharding Strategy (`_doesOrderBelongToShard`) based on `pkg:hashlib` `xxh32code`. This enables multiple queue display screens to filter requests randomly but deterministically locally without direct horizontal synchronization. + - Manages pagination computations (`_emitUpdatedPages`) dynamically re-calculating batch slots based on available spatial capacity. + +- `queue_home_view.dart`: + - **Description**: Scaffold implementation encapsulating the `OrderList`. + - **Core Logic**: Wraps the Queue Board inside a visual `_Marginalia` container. Implements a highly obfuscated UI hook (100x100 `MouseRegion` hit area in the top right corner) designed to secretly expose a `OrderBoardDebugOverlay` used for field administration of shards/timing configurations. + +- `fake_repositories.dart`: + - **Description**: Exposes `FakeLatteOrdersRepository` testing mocks allowing offline simulation. + +- `queue_settings.dart` / `.freezed.dart` / `.g.dart`: + - **Description**: A serializable (JSON via Freezed) model encapsulating tuning preferences for the local board (Shard Total, Shard Target, Update Frequency, Row Height) stored locally into `SharedPreferences`. + +**Dependencies/Relationships:** +- Strongly relies on `queue/widgets` like `OrderList` and `OrderBoardDebugOverlay` for component rendering. +- Subscribes via `LatteOrderMetadataBoardQueue` filter to actively listen to Firebase. diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/home/fake_repositories.dart b/genlatte/genlatte-ui/lib/src/screens/queue/home/fake_repositories.dart new file mode 100644 index 0000000..296ce51 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/home/fake_repositories.dart @@ -0,0 +1,288 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:data_layer/data_layer.dart'; +import 'package:genlatte/src/data/data.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:logging/logging.dart'; + +/// Fake repository for testing purposes. +class FakeLatteOrdersMetadataRepository extends Repository { + /// Creates a new instance of [FakeLatteOrdersMetadataRepository]. + FakeLatteOrdersMetadataRepository() + : super( + SourceList( + bindings: LatteOrderMetadata.bindings, + sources: >[ + FakeSource( + bindings: LatteOrderMetadata.bindings, + ), + ], + ), + ); +} + +/// Fake repository for testing purposes. +class FakeLatteOrdersRepository extends Repository + implements LatteOrdersRepository { + /// Creates a new instance of [FakeLatteOrdersRepository]. + FakeLatteOrdersRepository({ + AcceptImage acceptImage = _defaultAcceptImage, + CompleteOrder completeOrder = _defaultCompleteOrder, + GenerateRevisedImages generateRevisedImages = _defaultGenerateRevisedImages, + RejectImageBatch rejectImageBatch = _defaultRejectImageBatch, + SendToPrinters sendToPrinters = _defaultSendToPrinters, + SubmitOrder submitOrder = _defaultSubmitOrder, + }) : _acceptImage = acceptImage, + _completeOrder = completeOrder, + _generateRevisedImages = generateRevisedImages, + _rejectImageBatch = rejectImageBatch, + _sendToPrinters = sendToPrinters, + _submitOrder = submitOrder, + super( + SourceList( + bindings: LatteOrder.bindings, + sources: >[ + FakeSource( + bindings: LatteOrder.bindings, + ), + ], + ), + ); + + static final Logger _logger = Logger('$FakeLatteOrdersRepository'); + + @override + Future acceptImage({ + required String imageBatchId, + required String imageIndex, + }) => _acceptImage(imageBatchId: imageBatchId, imageIndex: imageIndex); + final AcceptImage _acceptImage; + + @override + Future completeOrder({ + required String orderId, + required String baristaId, + }) => _completeOrder(orderId: orderId, baristaId: baristaId); + final CompleteOrder _completeOrder; + + @override + Future generateRevisedImages({ + required String imageBatchId, + required String imageIndex, + required Map answers, + }) => _generateRevisedImages( + imageBatchId: imageBatchId, + imageIndex: imageIndex, + answers: answers, + ); + final GenerateRevisedImages _generateRevisedImages; + + @override + Future rejectImageBatch(String imageBatchId) => + _rejectImageBatch(imageBatchId); + final RejectImageBatch _rejectImageBatch; + + @override + Future sendToPrinters({ + required String imagePath, + required String machineName, + }) => _sendToPrinters(imagePath: imagePath, machineName: machineName); + final SendToPrinters _sendToPrinters; + + @override + Future submitOrder(String orderId) => _submitOrder(orderId); + final SubmitOrder _submitOrder; + + /// Duplicated code from [LatteOrdersRepository]. + @override + Future> toLattes(List metadatas) async { + final metadataMap = metadatas.toIdsMap(); + + final (orders, missingIds) = await getByIds( + metadataMap.keys.toSet(), + // [RequestType.global] (the default value) is fine for this because + // orders no longer change once they are on the Barista's screen; so + // fetching any locally cached records is guaranteed to be fine. + ); + + if (missingIds.isNotEmpty) { + throw DataException( + 'LatteMetaData Ids $missingIds lacked corresponding LatteOrders', + ); + } + + final lattes = []; + + // It is important to loop over [orders] as returned by [getByIds] and not + // over [event.metadatas] because any records lost (as represented by + // [missingIds]) will naturally be omitted in this way. This allows us to + // guarantee that [metadataMap[order.id!]] below will not be null. + for (final order in orders) { + final metadata = metadataMap[order.id!]!; + lattes.add(Latte(order: order, metadata: metadata)); + } + + return lattes; + } + + static Future _defaultAcceptImage({ + required String imageBatchId, + required String imageIndex, + }) async { + _logger.info('acceptImage: $imageBatchId - $imageIndex'); + } + + static Future _defaultCompleteOrder({ + required String orderId, + required String baristaId, + }) async { + _logger.info('completeOrder: $orderId - $baristaId'); + } + + static Future _defaultGenerateRevisedImages({ + required String imageBatchId, + required String imageIndex, + required Map answers, + }) async { + _logger.info( + 'generateRevisedImages: $imageBatchId - $imageIndex - $answers', + ); + } + + static Future _defaultSendToPrinters({ + required String imagePath, + required String machineName, + }) async { + _logger.info('sendToPrinters: $imagePath, $machineName'); + } + + static Future _defaultRejectImageBatch(String imageBatchId) async { + _logger.info('rejectImageBatch: $imageBatchId'); + } + + static Future _defaultSubmitOrder(String orderId) async { + _logger.info('submitOrder: $orderId'); + } +} + +/// Fake source for testing purposes. +class FakeSource extends Source with WatchableSource { + /// Creates a new instance of [FakeSource]. + FakeSource({required super.bindings}); + + int _id = 1; + + final StreamController _updateStream = StreamController.broadcast(); + + final Map _store = {}; + + @override + SourceType get sourceType => SourceType.local; + + @override + Future> delete(DeleteOperation operation) async { + _store.remove(operation.itemId); + _updateStream.add(null); + return DeleteResult.success(operation.details); + } + + @override + Future> getById(ReadOperation operation) async { + final item = _store[operation.itemId]; + return ReadResult.success(item, details: operation.details); + } + + @override + Future> getByIds(ReadByIdsOperation operation) async { + final items = operation.itemIds + .map((id) => _store[id]) + .whereType() + .toList(); + return ReadListResult.fromList( + items, + operation.details, + {}, // TODO(filiph): actually find missing ids. + bindings.getId, + ); + } + + @override + Future> getItems(ReadListOperation operation) async { + final items = _store.values.toList(); + return ReadListResult.fromList( + items, + operation.details, + {}, // TODO(filiph): actually find missing ids. + bindings.getId, + ); + } + + @override + Future> setItem(WriteOperation operation) async { + final existingId = bindings.getId(operation.item); + final id = existingId ?? '${_id++}'; + _store[id] = operation.item; + _updateStream.add(null); + return WriteResult.success(operation.item, details: operation.details); + } + + @override + Future> setItems(WriteListOperation operation) async { + for (final item in operation.items) { + final existingId = bindings.getId(item); + final id = existingId ?? '${_id++}'; + _store[id] = item; + } + _updateStream.add(null); + return WriteListResult.success(operation.items, details: operation.details); + } + + @override + Stream> watch(ReadOperation operation) async* { + ReadResult current = await getById(operation); + yield current; + await for (final _ in _updateStream.stream) { + final next = await getById(operation); + if (next.mapOrNull(success: (s) => s.item) != + current.mapOrNull(success: (s) => s.item)) { + yield next; + current = next; + } + } + } + + @override + Stream> watchByIds(ReadByIdsOperation operation) async* { + ReadListResult current = await getByIds(operation); + yield current; + await for (final _ in _updateStream.stream) { + final next = await getByIds(operation); + if (next.mapOrNull((s) => s.items) != current.mapOrNull((s) => s.items)) { + yield next; + current = next; + } + } + } + + @override + Stream> watchList(ReadListOperation operation) async* { + ReadListResult current = await getItems(operation); + yield current; + await for (final _ in _updateStream.stream) { + final next = await getItems(operation); + if (next.mapOrNull((s) => s.items) != current.mapOrNull((s) => s.items)) { + yield next; + current = next; + } + } + } + + /// Disposes of the [FakeSource]. + void dispose() { + _updateStream.close().ignore(); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home.dart b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home.dart new file mode 100644 index 0000000..4fdb0dd --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'queue_home_bloc.dart'; +export 'queue_home_view.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home_bloc.dart b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home_bloc.dart new file mode 100644 index 0000000..f3102a4 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home_bloc.dart @@ -0,0 +1,518 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; + +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte/src/data/data.dart'; +import 'package:genlatte/src/data/shared_preferences_repository.dart'; +import 'package:genlatte/src/screens/queue/home/queue_settings.dart'; +import 'package:genlatte/src/screens/queue/util/metadata_describe.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:hashlib/hashlib.dart'; +import 'package:logging/logging.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +part 'queue_home_bloc.freezed.dart'; + +final _logger = Logger('QueueHomeBloc'); + +typedef _Emit = Emitter; + +/// A page of orders. +final class OrderPage { + /// Creates a new [OrderPage]. + const OrderPage(this.orders); + + /// The orders to show on this page. + final List orders; +} + +/// {@template QueueHomeBloc} +/// {@endtemplate} +class QueueHomeBloc extends Bloc { + /// {@macro QueueHomeBloc} + QueueHomeBloc({ + required this.metadataRepository, + required this.ordersRepository, + required this.sharedPrefsRepository, + @visibleForTesting QueueSettingsState? initialSettings, + }) : super( + QueueHomeState( + settings: + initialSettings ?? + QueueSettingsState.load(sharedPrefsRepository), + ), + ) { + on( + (event, _Emit emit) => switch (event) { + OrdersUpdated() => _updateOrdersFromRepository(event, emit), + SetPageUpdatePeriod() => _saveNewPageUpdatePeriod(event, emit), + SetRecency() => _saveNewRecencySetting(event, emit), + SetShard() => _saveNewShardSetting(event, emit), + SetTargetRowHeight() => _emitNewTargetRowHeight(event, emit), + SetTopSettings() => _saveNewTopSetting(event, emit), + SlotsCounted() => _saveNewSlotsCount(event, emit), + RefreshPages() => _emitUpdatedPages(event, emit), + }, + ); + + _logger.info( + 'Creating a new $QueueHomeBloc ' + 'with shard ${state.settings} and $metadataRepository', + ); + + _ordersStreamSub = metadataRepository + .watchList( + details: RequestDetails.read( + // TODO(filiph): Maybe provide sharding information? + filter: const LatteOrderMetadataBoardQueue(), + ), + ) + .listen( + (metadata) => add(OrdersUpdated(metadata)), + onError: (Object e) => _logger.severe('Error when listening: $e', e), + ); + } + + /// Orders that were submitted way too long ago and never completed + /// should not be shown. + static const Duration _abandonedOrderAgeThreshold = Duration(days: 2); + + /// A set of orders (deduplicated on `id`, see [_createOrdersSet]) + /// that have been shown so far. + /// + /// This intentionally keeps orders that are no longer in the repository + /// because we might still need their data to fade them away. + HashSet _allOrders = _createOrdersSet(const []); + + /// The provided orders repository. + final LatteOrdersRepository ordersRepository; + + /// The provided metadata repository. + final Repository metadataRepository; + + /// The provided shared preferences repository. + final SharedPreferencesRepository sharedPrefsRepository; + + late final StreamSubscription> _ordersStreamSub; + + @override + Future close() async { + _ordersStreamSub.cancel().ignore(); + return super.close(); + } + + int _compareOrders(Latte a, Latte b) { + // First, sort by "status progress" (e.g., completed orders go before + // in-progress ones). + int roughPriority(Latte latte) => switch (latte.metadata.status) { + LatteOrderStatus.configuring => 100, + LatteOrderStatus.submitted => 10, + LatteOrderStatus.validated => 1, + LatteOrderStatus.inProgress => -1, + LatteOrderStatus.completed => -10, + LatteOrderStatus.archived => -100, + }; + + final aPriority = roughPriority(a); + final bPriority = roughPriority(b); + if (aPriority < bPriority) return -1; + if (aPriority > bPriority) return 1; + + final aSubmitted = a.metadata.orderSubmittedTime ?? DateTime(1970); + final bSubmitted = b.metadata.orderSubmittedTime ?? DateTime(1970); + return aSubmitted.compareTo(bSubmitted); + } + + void _emitNewTargetRowHeight(SetTargetRowHeight event, _Emit emit) { + _logger.info('_emitNewTargetRowHeight: $event'); + if (event.height < 1) { + _logger.warning('Invalid row height: ${event.height}'); + return; + } + + final newSettings = state.settings.copyWith( + targetRowHeight: event.height, + ); + _persistSettings(newSettings); + emit(state.copyWith(settings: newSettings)); + } + + void _emitUpdatedPages(RefreshPages event, _Emit emit) { + _logger.info('_emitUpdatedPages'); + + final slotCount = state.uiSlotCapacity; + if (slotCount == 0) { + _logger.info('new pages requested but there is no slot capacity'); + return; + } + + var noOrdersInAnyShard = true; + final now = DateTime.now(); + final shardValidOrders = _allOrders.where((latte) { + final submittedTime = latte.metadata.orderSubmittedTime; + if (submittedTime != null && + now.difference(submittedTime) > _abandonedOrderAgeThreshold) { + // Latte order is way too old. + return false; + } + + final completionTime = latte.metadata.completionTime; + if (completionTime != null && + now.difference(completionTime) > state.settings.maxShowAge) { + // Latte is stale. + return false; + } + + noOrdersInAnyShard = false; + + if (!_doesOrderBelongToShard( + latte, + shardNumber: state.settings.shardNumber, + shardsTotal: state.settings.shardTotal, + )) { + // Latte order doesn't belong here. + return false; + } + + return true; + }).toList()..sort(_compareOrders); + + final List ordersToShow; + if (state.settings.isTopScreen) { + // Only the first N orders are on the top screen. + ordersToShow = shardValidOrders + .take(state.settings.topOrdersCount) + .toList(); + } else { + // Only the orders N+1 and beyond are on other screens. + ordersToShow = shardValidOrders + .skip(state.settings.topOrdersCount) + .toList(); + } + + final pages = []; + for (var offset = 0; offset < ordersToShow.length; offset += slotCount) { + pages.add( + OrderPage( + List.generate( + slotCount, + (index) => ordersToShow.optGet(offset + index), + ), + ), + ); + } + + _logger.fine( + () => + 'RequestNewPages orders: ' + '${ordersToShow.map((o) => o.metadata.describe()).join(', ')}', + ); + + emit( + state.copyWith(shownPages: pages, noOrdersInAnyShard: noOrdersInAnyShard), + ); + } + + void _persistSettings(QueueSettingsState settings) { + sharedPrefsRepository + .setString( + QueueSettingsState.sharedPrefsKey, + jsonEncode(settings.toJson()), + ) + .ignore(); + } + + FutureOr _saveNewPageUpdatePeriod( + SetPageUpdatePeriod event, + _Emit emit, + ) { + _logger.info('_saveNewPageUpdatePeriod: $event'); + + final newSettings = state.settings.copyWith( + pageUpdatePeriod: event.pageUpdatePeriod, + ); + _persistSettings(newSettings); + emit(state.copyWith(settings: newSettings)); + add(const RefreshPages()); + } + + FutureOr _saveNewRecencySetting(SetRecency event, _Emit emit) { + _logger.info('_saveNewRecencySetting: $event'); + + final newSettings = state.settings.copyWith( + maxRecentAge: event.maxRecentAge, + maxShowAge: event.maxShowAge, + ); + _persistSettings(newSettings); + emit(state.copyWith(settings: newSettings)); + add(const RefreshPages()); + } + + void _saveNewShardSetting(SetShard event, _Emit emit) { + _logger.info( + '_saveNewShardSetting: ${event.shardNumber} of ${event.shardTotal}', + ); + if (event.shardTotal < 1) { + _logger.warning('Invalid $event: shard total cannot be below 1'); + return; + } + if (event.shardNumber < 1) { + _logger.warning('Invalid $event: shard number cannot be below 1'); + return; + } + if (event.shardNumber > event.shardTotal) { + _logger.warning( + 'Invalid $event: shard number cannot be above shard total', + ); + return; + } + + final newSettings = state.settings.copyWith( + shardNumber: event.shardNumber, + shardTotal: event.shardTotal, + ); + _persistSettings(newSettings); + emit(state.copyWith(settings: newSettings)); + add(const RefreshPages()); + } + + void _saveNewSlotsCount(SlotsCounted event, _Emit emit) { + _logger.info('_saveNewSlotsCount: $event'); + if (event.slotCount == state.uiSlotCapacity) return; + _logger.info('New slot count: ${event.slotCount}'); + emit(state.copyWith(uiSlotCapacity: event.slotCount)); + if (event.slotCount > 0) { + add(const RefreshPages()); + } + } + + void _saveNewTopSetting(SetTopSettings event, _Emit emit) { + _logger.info( + '_saveNewTopSetting: isTopScreen=${event.isTopScreen}, ' + 'topOrdersCount=${event.topOrdersCount}', + ); + if (event.topOrdersCount < 0) { + _logger.warning('Invalid $event: top orders count cannot be negative'); + return; + } + + final newSettings = state.settings.copyWith( + isTopScreen: event.isTopScreen, + topOrdersCount: event.topOrdersCount, + ); + _persistSettings(newSettings); + emit(state.copyWith(settings: newSettings)); + add(const RefreshPages()); + } + + Future _updateOrdersFromRepository( + OrdersUpdated event, + _Emit emit, + ) async { + _logger.fine( + '_updateOrdersFromRepository: ' + '${event.metadatas.map((m) => m.describe())}', + ); + + List lattes; + try { + lattes = await ordersRepository.toLattes(event.metadatas); + } on DataException catch (e) { + _logger.severe(e.message); + return; + } + + for (final latte in lattes) { + assert( + latte.metadata.status != .configuring, + 'Got an order that is still configuring: $latte', + ); + } + + _allOrders = _createOrdersSet( + // Note the order is important here. We want to keep the old orders around + // but updated orders have precedence. + lattes.followedBy(_allOrders), + ); + + add(const RefreshPages()); + } + + static HashSet _createOrdersSet(Iterable contents) => HashSet( + equals: (a, b) => a.metadata.id! == b.metadata.id!, + hashCode: (latte) => latte.metadata.id!.hashCode, + )..addAll(contents); + + /// ## Sharding strategy + /// + /// The queue app can be running at more than one display at a time. We need a + /// strategy to decide which order goes on which screen. Let's call this + /// "sharding". + /// + /// Constraints: + /// + /// - Sharding must work even if there's only one screen. At that point, all + /// orders must show on that screen. + /// - Sharding should work without the need for the running apps to + /// synchronize. One display does not need to know about the other. + /// - Both displays should have a comparable number of orders to display. It + /// shouldn't be usual to see many orders on one screen and no orders on the + /// others, for example. + /// - When situations arise during an event, it should be possible to adapt on + /// the spot. For example, if one display goes down, it shouldn't require + /// touching the database (change order metadata) to make the other + /// display(s) take over responsibilities. Similarly, if a display is added + /// on day 2 of event, no need to touch the database. + /// + /// Alternatives considered: + /// + /// - A continuous flow of orders from one display to the other. A single + /// list. This would require synchronization of the devices. It would be + /// common to have empty displays (except the first one). It would be gnarly + /// to figure out what happens when the alotted number of screens is not + /// enough to show all orders (how do we rotate something like this?). + /// - Have one screen for "serving now", and the second for all orders. + /// Doesn't solve for more than two displays. Doesn't scale down as easily + /// to one display. + /// - Server-driven sharding. Saves on traffic to clients but is much less + /// nimble, and requires more code. + /// + /// Solution: + /// + /// - For the sake of simplicity, let's assume there are two devices, Left + /// Display and Right Display. + /// - This means that `shardsTotal == 2`. We assume different shard numbers to + /// the displays, so Left Display can be `shardNumber == 1` and Right + /// Display can be `shardNumber == 2`. + /// - The Queue app has a "setup" overlay that lets us change the shards total + /// and the current display's shard number at runtime. + /// - Let's assume we're on Right Display, shard number is 2 (of 2). + /// - A new list of orders comes in from the Server. + /// - For each order to be displayed: + /// - We take its `id` --> a String like `cskAU2zymqpk0qOCRQb9`) + /// - We get a hash of the string --> a number like `4846121616` + /// - We modulo the hash by the number of shards --> a remainder like `0` + /// - We compare the remainder with the shard number (minus 1) --> `0 != 1` + /// - If the remainder doesn't equal the shard number, the order is not for + /// this screen + /// - The rest of the code just ignores the existence of the other orders. It + /// can decide to paginate the orders if it needs to, while the other screen + /// can decide not to paginate because it has fewer orders, and so on. + /// + /// There are some important details to consider: + /// + /// - The hashing function must have a reasonably uniform distribution. That's + /// why I switched from using `String.hashCode`, which sometimes had way too + /// many hits for one shard and way too few for the other. After switching + /// to `pkg:hashlib`, it's much better. Though it's still _possible_ to have + /// very uneven distribution between shards, it's much less likely. + /// - The model for `LatteOrder` suggests that `id` can be `null`, at which + /// point I'm not sure what to do. I assume this is pretty much guaranteed + /// to never happen (at least for orders seen by the client), + /// so we just show such order on all shards to be safe (and log a warning). + static bool _doesOrderBelongToShard( + Latte latte, { + required int shardNumber, + required int shardsTotal, + }) { + assert(shardsTotal > 0, 'Must have at least one shard'); + assert(shardNumber > 0, 'Shard number must be at least 1'); + assert( + shardNumber <= shardsTotal, + 'Shard number must never be higher than total number of shards', + ); + if (shardsTotal == 1) { + return true; + } + final id = latte.order.id; + if (id == null) { + _logger.warning('Order has null id: $latte'); + // It's probably safer to show a latte than to hide it. + return true; + } else { + // Instead of String.hashCode, which returns clustered hashes + // (meaning that one shard can easily get many more orders + // than all other shards), + // this `pkg:hashlib` function returns a more uniform distribution + // while still being very fast (it's not cryptographic). + final hash = xxh32code(id); + final modulo = hash % shardsTotal; + return modulo == (shardNumber - 1); + } + } +} + +/// Actions that can be taken on the QueueHome page. +@Freezed() +sealed class QueueHomeEvent with _$QueueHomeEvent { + /// This event is generated when the set of orders is updated in the + /// repository (typically in Firebase). + const factory QueueHomeEvent.ordersUpdated( + List metadatas, + ) = OrdersUpdated; + + /// Request for new pages, immediately. + const factory QueueHomeEvent.refreshPages() = RefreshPages; + + /// Sets up a new custom page update period. + const factory QueueHomeEvent.setPageUpdatePeriod(Duration pageUpdatePeriod) = + SetPageUpdatePeriod; + + /// Sets up new recency thresholds. + const factory QueueHomeEvent.setRecency( + Duration maxRecentAge, + Duration maxShowAge, + ) = SetRecency; + + /// Sets up new sharding settings. + const factory QueueHomeEvent.setShard(int shardNumber, int shardTotal) = + SetShard; + + /// Sets up a new target row height. + const factory QueueHomeEvent.setTargetRowHeight(double height) = + SetTargetRowHeight; + + /// Sets up top settings. + const factory QueueHomeEvent.setTopSettings({ + required bool isTopScreen, + required int topOrdersCount, + }) = SetTopSettings; + + /// Event sent by the view to inform the bloc about available space. + const factory QueueHomeEvent.slotsCounted( + int slotCount, + ) = SlotsCounted; +} + +/// {@template QueueHomeState} +/// Complete representation of the QueueHome page's state. +/// {@endtemplate +@Freezed() +sealed class QueueHomeState with _$QueueHomeState { + /// {@macro QueueHomeState} + const factory QueueHomeState({ + /// The setup of the queue. Persisted. + required QueueSettingsState settings, + + /// The number of orders that the UI can show at a time. Depends on screen + /// configuration, and informs how many orders will be in each [OrderPage]. + @Default(0) int uiSlotCapacity, + + /// The orders to show. Will be split into pages by [uiSlotCapacity]. + @Default([]) List shownPages, + + /// When this is true, we know that no orders are currently in the pipeline. + /// This is different from simply no orders being available + /// for _this shard_. The UI can show a message or a "screen saver". + @Default(false) bool noOrdersInAnyShard, + }) = _QueueHomeState; + + const QueueHomeState._(); +} diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home_bloc.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home_bloc.freezed.dart new file mode 100644 index 0000000..1efd80b --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home_bloc.freezed.dart @@ -0,0 +1,1019 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'queue_home_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$QueueHomeEvent { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QueueHomeEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QueueHomeEvent()'; +} + + +} + +/// @nodoc +class $QueueHomeEventCopyWith<$Res> { +$QueueHomeEventCopyWith(QueueHomeEvent _, $Res Function(QueueHomeEvent) __); +} + + +/// Adds pattern-matching-related methods to [QueueHomeEvent]. +extension QueueHomeEventPatterns on QueueHomeEvent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( OrdersUpdated value)? ordersUpdated,TResult Function( RefreshPages value)? refreshPages,TResult Function( SetPageUpdatePeriod value)? setPageUpdatePeriod,TResult Function( SetRecency value)? setRecency,TResult Function( SetShard value)? setShard,TResult Function( SetTargetRowHeight value)? setTargetRowHeight,TResult Function( SetTopSettings value)? setTopSettings,TResult Function( SlotsCounted value)? slotsCounted,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case OrdersUpdated() when ordersUpdated != null: +return ordersUpdated(_that);case RefreshPages() when refreshPages != null: +return refreshPages(_that);case SetPageUpdatePeriod() when setPageUpdatePeriod != null: +return setPageUpdatePeriod(_that);case SetRecency() when setRecency != null: +return setRecency(_that);case SetShard() when setShard != null: +return setShard(_that);case SetTargetRowHeight() when setTargetRowHeight != null: +return setTargetRowHeight(_that);case SetTopSettings() when setTopSettings != null: +return setTopSettings(_that);case SlotsCounted() when slotsCounted != null: +return slotsCounted(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( OrdersUpdated value) ordersUpdated,required TResult Function( RefreshPages value) refreshPages,required TResult Function( SetPageUpdatePeriod value) setPageUpdatePeriod,required TResult Function( SetRecency value) setRecency,required TResult Function( SetShard value) setShard,required TResult Function( SetTargetRowHeight value) setTargetRowHeight,required TResult Function( SetTopSettings value) setTopSettings,required TResult Function( SlotsCounted value) slotsCounted,}){ +final _that = this; +switch (_that) { +case OrdersUpdated(): +return ordersUpdated(_that);case RefreshPages(): +return refreshPages(_that);case SetPageUpdatePeriod(): +return setPageUpdatePeriod(_that);case SetRecency(): +return setRecency(_that);case SetShard(): +return setShard(_that);case SetTargetRowHeight(): +return setTargetRowHeight(_that);case SetTopSettings(): +return setTopSettings(_that);case SlotsCounted(): +return slotsCounted(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( OrdersUpdated value)? ordersUpdated,TResult? Function( RefreshPages value)? refreshPages,TResult? Function( SetPageUpdatePeriod value)? setPageUpdatePeriod,TResult? Function( SetRecency value)? setRecency,TResult? Function( SetShard value)? setShard,TResult? Function( SetTargetRowHeight value)? setTargetRowHeight,TResult? Function( SetTopSettings value)? setTopSettings,TResult? Function( SlotsCounted value)? slotsCounted,}){ +final _that = this; +switch (_that) { +case OrdersUpdated() when ordersUpdated != null: +return ordersUpdated(_that);case RefreshPages() when refreshPages != null: +return refreshPages(_that);case SetPageUpdatePeriod() when setPageUpdatePeriod != null: +return setPageUpdatePeriod(_that);case SetRecency() when setRecency != null: +return setRecency(_that);case SetShard() when setShard != null: +return setShard(_that);case SetTargetRowHeight() when setTargetRowHeight != null: +return setTargetRowHeight(_that);case SetTopSettings() when setTopSettings != null: +return setTopSettings(_that);case SlotsCounted() when slotsCounted != null: +return slotsCounted(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( List metadatas)? ordersUpdated,TResult Function()? refreshPages,TResult Function( Duration pageUpdatePeriod)? setPageUpdatePeriod,TResult Function( Duration maxRecentAge, Duration maxShowAge)? setRecency,TResult Function( int shardNumber, int shardTotal)? setShard,TResult Function( double height)? setTargetRowHeight,TResult Function( bool isTopScreen, int topOrdersCount)? setTopSettings,TResult Function( int slotCount)? slotsCounted,required TResult orElse(),}) {final _that = this; +switch (_that) { +case OrdersUpdated() when ordersUpdated != null: +return ordersUpdated(_that.metadatas);case RefreshPages() when refreshPages != null: +return refreshPages();case SetPageUpdatePeriod() when setPageUpdatePeriod != null: +return setPageUpdatePeriod(_that.pageUpdatePeriod);case SetRecency() when setRecency != null: +return setRecency(_that.maxRecentAge,_that.maxShowAge);case SetShard() when setShard != null: +return setShard(_that.shardNumber,_that.shardTotal);case SetTargetRowHeight() when setTargetRowHeight != null: +return setTargetRowHeight(_that.height);case SetTopSettings() when setTopSettings != null: +return setTopSettings(_that.isTopScreen,_that.topOrdersCount);case SlotsCounted() when slotsCounted != null: +return slotsCounted(_that.slotCount);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( List metadatas) ordersUpdated,required TResult Function() refreshPages,required TResult Function( Duration pageUpdatePeriod) setPageUpdatePeriod,required TResult Function( Duration maxRecentAge, Duration maxShowAge) setRecency,required TResult Function( int shardNumber, int shardTotal) setShard,required TResult Function( double height) setTargetRowHeight,required TResult Function( bool isTopScreen, int topOrdersCount) setTopSettings,required TResult Function( int slotCount) slotsCounted,}) {final _that = this; +switch (_that) { +case OrdersUpdated(): +return ordersUpdated(_that.metadatas);case RefreshPages(): +return refreshPages();case SetPageUpdatePeriod(): +return setPageUpdatePeriod(_that.pageUpdatePeriod);case SetRecency(): +return setRecency(_that.maxRecentAge,_that.maxShowAge);case SetShard(): +return setShard(_that.shardNumber,_that.shardTotal);case SetTargetRowHeight(): +return setTargetRowHeight(_that.height);case SetTopSettings(): +return setTopSettings(_that.isTopScreen,_that.topOrdersCount);case SlotsCounted(): +return slotsCounted(_that.slotCount);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( List metadatas)? ordersUpdated,TResult? Function()? refreshPages,TResult? Function( Duration pageUpdatePeriod)? setPageUpdatePeriod,TResult? Function( Duration maxRecentAge, Duration maxShowAge)? setRecency,TResult? Function( int shardNumber, int shardTotal)? setShard,TResult? Function( double height)? setTargetRowHeight,TResult? Function( bool isTopScreen, int topOrdersCount)? setTopSettings,TResult? Function( int slotCount)? slotsCounted,}) {final _that = this; +switch (_that) { +case OrdersUpdated() when ordersUpdated != null: +return ordersUpdated(_that.metadatas);case RefreshPages() when refreshPages != null: +return refreshPages();case SetPageUpdatePeriod() when setPageUpdatePeriod != null: +return setPageUpdatePeriod(_that.pageUpdatePeriod);case SetRecency() when setRecency != null: +return setRecency(_that.maxRecentAge,_that.maxShowAge);case SetShard() when setShard != null: +return setShard(_that.shardNumber,_that.shardTotal);case SetTargetRowHeight() when setTargetRowHeight != null: +return setTargetRowHeight(_that.height);case SetTopSettings() when setTopSettings != null: +return setTopSettings(_that.isTopScreen,_that.topOrdersCount);case SlotsCounted() when slotsCounted != null: +return slotsCounted(_that.slotCount);case _: + return null; + +} +} + +} + +/// @nodoc + + +class OrdersUpdated implements QueueHomeEvent { + const OrdersUpdated(final List metadatas): _metadatas = metadatas; + + + final List _metadatas; + List get metadatas { + if (_metadatas is EqualUnmodifiableListView) return _metadatas; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_metadatas); +} + + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$OrdersUpdatedCopyWith get copyWith => _$OrdersUpdatedCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is OrdersUpdated&&const DeepCollectionEquality().equals(other._metadatas, _metadatas)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_metadatas)); + +@override +String toString() { + return 'QueueHomeEvent.ordersUpdated(metadatas: $metadatas)'; +} + + +} + +/// @nodoc +abstract mixin class $OrdersUpdatedCopyWith<$Res> implements $QueueHomeEventCopyWith<$Res> { + factory $OrdersUpdatedCopyWith(OrdersUpdated value, $Res Function(OrdersUpdated) _then) = _$OrdersUpdatedCopyWithImpl; +@useResult +$Res call({ + List metadatas +}); + + + + +} +/// @nodoc +class _$OrdersUpdatedCopyWithImpl<$Res> + implements $OrdersUpdatedCopyWith<$Res> { + _$OrdersUpdatedCopyWithImpl(this._self, this._then); + + final OrdersUpdated _self; + final $Res Function(OrdersUpdated) _then; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? metadatas = null,}) { + return _then(OrdersUpdated( +null == metadatas ? _self._metadatas : metadatas // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class RefreshPages implements QueueHomeEvent { + const RefreshPages(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RefreshPages); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'QueueHomeEvent.refreshPages()'; +} + + +} + + + + +/// @nodoc + + +class SetPageUpdatePeriod implements QueueHomeEvent { + const SetPageUpdatePeriod(this.pageUpdatePeriod); + + + final Duration pageUpdatePeriod; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SetPageUpdatePeriodCopyWith get copyWith => _$SetPageUpdatePeriodCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SetPageUpdatePeriod&&(identical(other.pageUpdatePeriod, pageUpdatePeriod) || other.pageUpdatePeriod == pageUpdatePeriod)); +} + + +@override +int get hashCode => Object.hash(runtimeType,pageUpdatePeriod); + +@override +String toString() { + return 'QueueHomeEvent.setPageUpdatePeriod(pageUpdatePeriod: $pageUpdatePeriod)'; +} + + +} + +/// @nodoc +abstract mixin class $SetPageUpdatePeriodCopyWith<$Res> implements $QueueHomeEventCopyWith<$Res> { + factory $SetPageUpdatePeriodCopyWith(SetPageUpdatePeriod value, $Res Function(SetPageUpdatePeriod) _then) = _$SetPageUpdatePeriodCopyWithImpl; +@useResult +$Res call({ + Duration pageUpdatePeriod +}); + + + + +} +/// @nodoc +class _$SetPageUpdatePeriodCopyWithImpl<$Res> + implements $SetPageUpdatePeriodCopyWith<$Res> { + _$SetPageUpdatePeriodCopyWithImpl(this._self, this._then); + + final SetPageUpdatePeriod _self; + final $Res Function(SetPageUpdatePeriod) _then; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? pageUpdatePeriod = null,}) { + return _then(SetPageUpdatePeriod( +null == pageUpdatePeriod ? _self.pageUpdatePeriod : pageUpdatePeriod // ignore: cast_nullable_to_non_nullable +as Duration, + )); +} + + +} + +/// @nodoc + + +class SetRecency implements QueueHomeEvent { + const SetRecency(this.maxRecentAge, this.maxShowAge); + + + final Duration maxRecentAge; + final Duration maxShowAge; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SetRecencyCopyWith get copyWith => _$SetRecencyCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SetRecency&&(identical(other.maxRecentAge, maxRecentAge) || other.maxRecentAge == maxRecentAge)&&(identical(other.maxShowAge, maxShowAge) || other.maxShowAge == maxShowAge)); +} + + +@override +int get hashCode => Object.hash(runtimeType,maxRecentAge,maxShowAge); + +@override +String toString() { + return 'QueueHomeEvent.setRecency(maxRecentAge: $maxRecentAge, maxShowAge: $maxShowAge)'; +} + + +} + +/// @nodoc +abstract mixin class $SetRecencyCopyWith<$Res> implements $QueueHomeEventCopyWith<$Res> { + factory $SetRecencyCopyWith(SetRecency value, $Res Function(SetRecency) _then) = _$SetRecencyCopyWithImpl; +@useResult +$Res call({ + Duration maxRecentAge, Duration maxShowAge +}); + + + + +} +/// @nodoc +class _$SetRecencyCopyWithImpl<$Res> + implements $SetRecencyCopyWith<$Res> { + _$SetRecencyCopyWithImpl(this._self, this._then); + + final SetRecency _self; + final $Res Function(SetRecency) _then; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? maxRecentAge = null,Object? maxShowAge = null,}) { + return _then(SetRecency( +null == maxRecentAge ? _self.maxRecentAge : maxRecentAge // ignore: cast_nullable_to_non_nullable +as Duration,null == maxShowAge ? _self.maxShowAge : maxShowAge // ignore: cast_nullable_to_non_nullable +as Duration, + )); +} + + +} + +/// @nodoc + + +class SetShard implements QueueHomeEvent { + const SetShard(this.shardNumber, this.shardTotal); + + + final int shardNumber; + final int shardTotal; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SetShardCopyWith get copyWith => _$SetShardCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SetShard&&(identical(other.shardNumber, shardNumber) || other.shardNumber == shardNumber)&&(identical(other.shardTotal, shardTotal) || other.shardTotal == shardTotal)); +} + + +@override +int get hashCode => Object.hash(runtimeType,shardNumber,shardTotal); + +@override +String toString() { + return 'QueueHomeEvent.setShard(shardNumber: $shardNumber, shardTotal: $shardTotal)'; +} + + +} + +/// @nodoc +abstract mixin class $SetShardCopyWith<$Res> implements $QueueHomeEventCopyWith<$Res> { + factory $SetShardCopyWith(SetShard value, $Res Function(SetShard) _then) = _$SetShardCopyWithImpl; +@useResult +$Res call({ + int shardNumber, int shardTotal +}); + + + + +} +/// @nodoc +class _$SetShardCopyWithImpl<$Res> + implements $SetShardCopyWith<$Res> { + _$SetShardCopyWithImpl(this._self, this._then); + + final SetShard _self; + final $Res Function(SetShard) _then; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? shardNumber = null,Object? shardTotal = null,}) { + return _then(SetShard( +null == shardNumber ? _self.shardNumber : shardNumber // ignore: cast_nullable_to_non_nullable +as int,null == shardTotal ? _self.shardTotal : shardTotal // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +/// @nodoc + + +class SetTargetRowHeight implements QueueHomeEvent { + const SetTargetRowHeight(this.height); + + + final double height; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SetTargetRowHeightCopyWith get copyWith => _$SetTargetRowHeightCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SetTargetRowHeight&&(identical(other.height, height) || other.height == height)); +} + + +@override +int get hashCode => Object.hash(runtimeType,height); + +@override +String toString() { + return 'QueueHomeEvent.setTargetRowHeight(height: $height)'; +} + + +} + +/// @nodoc +abstract mixin class $SetTargetRowHeightCopyWith<$Res> implements $QueueHomeEventCopyWith<$Res> { + factory $SetTargetRowHeightCopyWith(SetTargetRowHeight value, $Res Function(SetTargetRowHeight) _then) = _$SetTargetRowHeightCopyWithImpl; +@useResult +$Res call({ + double height +}); + + + + +} +/// @nodoc +class _$SetTargetRowHeightCopyWithImpl<$Res> + implements $SetTargetRowHeightCopyWith<$Res> { + _$SetTargetRowHeightCopyWithImpl(this._self, this._then); + + final SetTargetRowHeight _self; + final $Res Function(SetTargetRowHeight) _then; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? height = null,}) { + return _then(SetTargetRowHeight( +null == height ? _self.height : height // ignore: cast_nullable_to_non_nullable +as double, + )); +} + + +} + +/// @nodoc + + +class SetTopSettings implements QueueHomeEvent { + const SetTopSettings({required this.isTopScreen, required this.topOrdersCount}); + + + final bool isTopScreen; + final int topOrdersCount; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SetTopSettingsCopyWith get copyWith => _$SetTopSettingsCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SetTopSettings&&(identical(other.isTopScreen, isTopScreen) || other.isTopScreen == isTopScreen)&&(identical(other.topOrdersCount, topOrdersCount) || other.topOrdersCount == topOrdersCount)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isTopScreen,topOrdersCount); + +@override +String toString() { + return 'QueueHomeEvent.setTopSettings(isTopScreen: $isTopScreen, topOrdersCount: $topOrdersCount)'; +} + + +} + +/// @nodoc +abstract mixin class $SetTopSettingsCopyWith<$Res> implements $QueueHomeEventCopyWith<$Res> { + factory $SetTopSettingsCopyWith(SetTopSettings value, $Res Function(SetTopSettings) _then) = _$SetTopSettingsCopyWithImpl; +@useResult +$Res call({ + bool isTopScreen, int topOrdersCount +}); + + + + +} +/// @nodoc +class _$SetTopSettingsCopyWithImpl<$Res> + implements $SetTopSettingsCopyWith<$Res> { + _$SetTopSettingsCopyWithImpl(this._self, this._then); + + final SetTopSettings _self; + final $Res Function(SetTopSettings) _then; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? isTopScreen = null,Object? topOrdersCount = null,}) { + return _then(SetTopSettings( +isTopScreen: null == isTopScreen ? _self.isTopScreen : isTopScreen // ignore: cast_nullable_to_non_nullable +as bool,topOrdersCount: null == topOrdersCount ? _self.topOrdersCount : topOrdersCount // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +/// @nodoc + + +class SlotsCounted implements QueueHomeEvent { + const SlotsCounted(this.slotCount); + + + final int slotCount; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SlotsCountedCopyWith get copyWith => _$SlotsCountedCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SlotsCounted&&(identical(other.slotCount, slotCount) || other.slotCount == slotCount)); +} + + +@override +int get hashCode => Object.hash(runtimeType,slotCount); + +@override +String toString() { + return 'QueueHomeEvent.slotsCounted(slotCount: $slotCount)'; +} + + +} + +/// @nodoc +abstract mixin class $SlotsCountedCopyWith<$Res> implements $QueueHomeEventCopyWith<$Res> { + factory $SlotsCountedCopyWith(SlotsCounted value, $Res Function(SlotsCounted) _then) = _$SlotsCountedCopyWithImpl; +@useResult +$Res call({ + int slotCount +}); + + + + +} +/// @nodoc +class _$SlotsCountedCopyWithImpl<$Res> + implements $SlotsCountedCopyWith<$Res> { + _$SlotsCountedCopyWithImpl(this._self, this._then); + + final SlotsCounted _self; + final $Res Function(SlotsCounted) _then; + +/// Create a copy of QueueHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? slotCount = null,}) { + return _then(SlotsCounted( +null == slotCount ? _self.slotCount : slotCount // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +/// @nodoc +mixin _$QueueHomeState { + +/// The setup of the queue. Persisted. + QueueSettingsState get settings;/// The number of orders that the UI can show at a time. Depends on screen +/// configuration, and informs how many orders will be in each [OrderPage]. + int get uiSlotCapacity;/// The orders to show. Will be split into pages by [uiSlotCapacity]. + List get shownPages;/// When this is true, we know that no orders are currently in the pipeline. +/// This is different from simply no orders being available +/// for _this shard_. The UI can show a message or a "screen saver". + bool get noOrdersInAnyShard; +/// Create a copy of QueueHomeState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QueueHomeStateCopyWith get copyWith => _$QueueHomeStateCopyWithImpl(this as QueueHomeState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QueueHomeState&&(identical(other.settings, settings) || other.settings == settings)&&(identical(other.uiSlotCapacity, uiSlotCapacity) || other.uiSlotCapacity == uiSlotCapacity)&&const DeepCollectionEquality().equals(other.shownPages, shownPages)&&(identical(other.noOrdersInAnyShard, noOrdersInAnyShard) || other.noOrdersInAnyShard == noOrdersInAnyShard)); +} + + +@override +int get hashCode => Object.hash(runtimeType,settings,uiSlotCapacity,const DeepCollectionEquality().hash(shownPages),noOrdersInAnyShard); + +@override +String toString() { + return 'QueueHomeState(settings: $settings, uiSlotCapacity: $uiSlotCapacity, shownPages: $shownPages, noOrdersInAnyShard: $noOrdersInAnyShard)'; +} + + +} + +/// @nodoc +abstract mixin class $QueueHomeStateCopyWith<$Res> { + factory $QueueHomeStateCopyWith(QueueHomeState value, $Res Function(QueueHomeState) _then) = _$QueueHomeStateCopyWithImpl; +@useResult +$Res call({ + QueueSettingsState settings, int uiSlotCapacity, List shownPages, bool noOrdersInAnyShard +}); + + +$QueueSettingsStateCopyWith<$Res> get settings; + +} +/// @nodoc +class _$QueueHomeStateCopyWithImpl<$Res> + implements $QueueHomeStateCopyWith<$Res> { + _$QueueHomeStateCopyWithImpl(this._self, this._then); + + final QueueHomeState _self; + final $Res Function(QueueHomeState) _then; + +/// Create a copy of QueueHomeState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? settings = null,Object? uiSlotCapacity = null,Object? shownPages = null,Object? noOrdersInAnyShard = null,}) { + return _then(_self.copyWith( +settings: null == settings ? _self.settings : settings // ignore: cast_nullable_to_non_nullable +as QueueSettingsState,uiSlotCapacity: null == uiSlotCapacity ? _self.uiSlotCapacity : uiSlotCapacity // ignore: cast_nullable_to_non_nullable +as int,shownPages: null == shownPages ? _self.shownPages : shownPages // ignore: cast_nullable_to_non_nullable +as List,noOrdersInAnyShard: null == noOrdersInAnyShard ? _self.noOrdersInAnyShard : noOrdersInAnyShard // ignore: cast_nullable_to_non_nullable +as bool, + )); +} +/// Create a copy of QueueHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$QueueSettingsStateCopyWith<$Res> get settings { + + return $QueueSettingsStateCopyWith<$Res>(_self.settings, (value) { + return _then(_self.copyWith(settings: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [QueueHomeState]. +extension QueueHomeStatePatterns on QueueHomeState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _QueueHomeState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _QueueHomeState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _QueueHomeState value) $default,){ +final _that = this; +switch (_that) { +case _QueueHomeState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _QueueHomeState value)? $default,){ +final _that = this; +switch (_that) { +case _QueueHomeState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( QueueSettingsState settings, int uiSlotCapacity, List shownPages, bool noOrdersInAnyShard)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _QueueHomeState() when $default != null: +return $default(_that.settings,_that.uiSlotCapacity,_that.shownPages,_that.noOrdersInAnyShard);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( QueueSettingsState settings, int uiSlotCapacity, List shownPages, bool noOrdersInAnyShard) $default,) {final _that = this; +switch (_that) { +case _QueueHomeState(): +return $default(_that.settings,_that.uiSlotCapacity,_that.shownPages,_that.noOrdersInAnyShard);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( QueueSettingsState settings, int uiSlotCapacity, List shownPages, bool noOrdersInAnyShard)? $default,) {final _that = this; +switch (_that) { +case _QueueHomeState() when $default != null: +return $default(_that.settings,_that.uiSlotCapacity,_that.shownPages,_that.noOrdersInAnyShard);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _QueueHomeState extends QueueHomeState { + const _QueueHomeState({required this.settings, this.uiSlotCapacity = 0, final List shownPages = const [], this.noOrdersInAnyShard = false}): _shownPages = shownPages,super._(); + + +/// The setup of the queue. Persisted. +@override final QueueSettingsState settings; +/// The number of orders that the UI can show at a time. Depends on screen +/// configuration, and informs how many orders will be in each [OrderPage]. +@override@JsonKey() final int uiSlotCapacity; +/// The orders to show. Will be split into pages by [uiSlotCapacity]. + final List _shownPages; +/// The orders to show. Will be split into pages by [uiSlotCapacity]. +@override@JsonKey() List get shownPages { + if (_shownPages is EqualUnmodifiableListView) return _shownPages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_shownPages); +} + +/// When this is true, we know that no orders are currently in the pipeline. +/// This is different from simply no orders being available +/// for _this shard_. The UI can show a message or a "screen saver". +@override@JsonKey() final bool noOrdersInAnyShard; + +/// Create a copy of QueueHomeState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$QueueHomeStateCopyWith<_QueueHomeState> get copyWith => __$QueueHomeStateCopyWithImpl<_QueueHomeState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _QueueHomeState&&(identical(other.settings, settings) || other.settings == settings)&&(identical(other.uiSlotCapacity, uiSlotCapacity) || other.uiSlotCapacity == uiSlotCapacity)&&const DeepCollectionEquality().equals(other._shownPages, _shownPages)&&(identical(other.noOrdersInAnyShard, noOrdersInAnyShard) || other.noOrdersInAnyShard == noOrdersInAnyShard)); +} + + +@override +int get hashCode => Object.hash(runtimeType,settings,uiSlotCapacity,const DeepCollectionEquality().hash(_shownPages),noOrdersInAnyShard); + +@override +String toString() { + return 'QueueHomeState(settings: $settings, uiSlotCapacity: $uiSlotCapacity, shownPages: $shownPages, noOrdersInAnyShard: $noOrdersInAnyShard)'; +} + + +} + +/// @nodoc +abstract mixin class _$QueueHomeStateCopyWith<$Res> implements $QueueHomeStateCopyWith<$Res> { + factory _$QueueHomeStateCopyWith(_QueueHomeState value, $Res Function(_QueueHomeState) _then) = __$QueueHomeStateCopyWithImpl; +@override @useResult +$Res call({ + QueueSettingsState settings, int uiSlotCapacity, List shownPages, bool noOrdersInAnyShard +}); + + +@override $QueueSettingsStateCopyWith<$Res> get settings; + +} +/// @nodoc +class __$QueueHomeStateCopyWithImpl<$Res> + implements _$QueueHomeStateCopyWith<$Res> { + __$QueueHomeStateCopyWithImpl(this._self, this._then); + + final _QueueHomeState _self; + final $Res Function(_QueueHomeState) _then; + +/// Create a copy of QueueHomeState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? settings = null,Object? uiSlotCapacity = null,Object? shownPages = null,Object? noOrdersInAnyShard = null,}) { + return _then(_QueueHomeState( +settings: null == settings ? _self.settings : settings // ignore: cast_nullable_to_non_nullable +as QueueSettingsState,uiSlotCapacity: null == uiSlotCapacity ? _self.uiSlotCapacity : uiSlotCapacity // ignore: cast_nullable_to_non_nullable +as int,shownPages: null == shownPages ? _self._shownPages : shownPages // ignore: cast_nullable_to_non_nullable +as List,noOrdersInAnyShard: null == noOrdersInAnyShard ? _self.noOrdersInAnyShard : noOrdersInAnyShard // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +/// Create a copy of QueueHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$QueueSettingsStateCopyWith<$Res> get settings { + + return $QueueSettingsStateCopyWith<$Res>(_self.settings, (value) { + return _then(_self.copyWith(settings: value)); + }); +} +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home_view.dart b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home_view.dart new file mode 100644 index 0000000..cd10c56 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_home_view.dart @@ -0,0 +1,156 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/data/data.dart'; +import 'package:genlatte/src/data/shared_preferences_repository.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/queue/home/fake_repositories.dart'; +import 'package:genlatte/src/screens/queue/home/queue_home.dart'; +import 'package:genlatte/src/screens/queue/widgets/debug_overlay.dart'; +import 'package:genlatte/src/screens/queue/widgets/order_list.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// {@template QueueHomeScreen} +/// Initial QueueHome screen. +/// {@endtemplate} +class QueueHomeScreen extends StatefulWidget { + /// {@macro QueueHomeScreen} + const QueueHomeScreen({super.key}); + + @override + State createState() => _QueueHomeScreenState(); +} + +class _QueueHomeScreenState extends State { + late QueueHomeBloc bloc; + + FakeLatteOrdersRepository? _fakeOrdersRepository; + + FakeLatteOrdersMetadataRepository? _fakeMetadataRepository; + + bool _showDebugOverlay = false; + + bool _hoverDebugOverlay = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.white, + child: Stack( + children: [ + BlocBuilder( + bloc: bloc, + buildWhen: (previous, current) => + previous.shownPages != current.shownPages || + previous.settings != current.settings, + builder: (context, state) => OrderList( + pages: state.shownPages, + onSlotsCounted: (slots) => bloc.add(SlotsCounted(slots)), + maxRecentAge: state.settings.maxRecentAge, + targetRowHeight: state.settings.targetRowHeight, + noOrdersInAnyShard: state.noOrdersInAnyShard, + pageUpdatePeriod: state.settings.pageUpdatePeriod, + ), + ), + Positioned( + width: 100, + height: 100, + top: 10, + right: 10, + child: StatefulBuilder( + builder: (context, innerSetState) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => + innerSetState(() => _hoverDebugOverlay = true), + onExit: (_) => + innerSetState(() => _hoverDebugOverlay = false), + child: ColoredBox( + color: _hoverDebugOverlay + ? AppColors.chevronYellow.withValues(alpha: 0.75) + : AppColors.transparent, + child: GestureDetector( + key: const Key('hidden_overlay_show_toggle'), + onTap: () => setState( + () => _showDebugOverlay = !_showDebugOverlay, + ), + + behavior: .opaque, + ), + ), + ); + }, + ), + ), + if (_showDebugOverlay) + Positioned( + top: 120, + bottom: 20, + right: 20, + child: Align( + alignment: .bottomEnd, + child: Opacity( + opacity: 0.9, + child: SingleChildScrollView( + child: OrderBoardDebugOverlay( + bloc: bloc, + ordersRepository: _fakeOrdersRepository, + metadataRepository: _fakeMetadataRepository, + ), + ), + ), + ), + ), + ], + ), + ); + } + + @override + void didUpdateWidget(covariant QueueHomeScreen oldWidget) { + super.didUpdateWidget(oldWidget); + + final previousBloc = bloc; + bloc = _setupBloc(); + previousBloc.close().ignore(); + } + + @override + Future dispose() async { + unawaited(bloc.close()); + super.dispose(); + } + + @override + void initState() { + super.initState(); + bloc = _setupBloc(); + } + + QueueHomeBloc _setupBloc() { + final LatteOrdersRepository ordersRepo; + final Repository metadataRepo; + // if (widget.useFakes) { + // ordersRepo = _fakeOrdersRepository = FakeLatteOrdersRepository(); + // metadataRepo = _fakeMetadataRepository = + // FakeLatteOrdersMetadataRepository(); + // } else { + ordersRepo = GetIt.I(); + metadataRepo = GetIt.I>(); + // } + final sharedPrefsRepo = GetIt.I(); + + return QueueHomeBloc( + ordersRepository: ordersRepo, + metadataRepository: metadataRepo, + sharedPrefsRepository: sharedPrefsRepo, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_settings.dart b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_settings.dart new file mode 100644 index 0000000..e13e750 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_settings.dart @@ -0,0 +1,74 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte/src/data/shared_preferences_repository.dart'; +import 'package:logging/logging.dart'; + +part 'queue_settings.freezed.dart'; +part 'queue_settings.g.dart'; + +@Freezed() +sealed class QueueSettingsState with _$QueueSettingsState { + /// {@macro QueueSettings} + const factory QueueSettingsState({ + /// If set to `true`, this screen will only show the first page and will + /// never paginate. + /// + /// If set to `false`, this screen works normally except it ignores + /// the first [topOrdersCount] of orders. + @Default(false) bool isTopScreen, + + /// Asks the screen to ignore the first N orders (because, presumably, + /// they are shown on the top screen (see [isTopScreen]). + @Default(0) int topOrdersCount, + + /// The shard number of this device. + @Default(1) int shardNumber, + + /// The total number of shards in play. + @Default(1) int shardTotal, + + /// The maximum age of a completed order, after which the order will not + /// be shown anymore. + @Default(Duration(minutes: 15)) Duration maxShowAge, + + /// The duration for which an order is considered "recent". The UI may + /// choose to highlight such orders. + @Default(Duration(minutes: 5)) Duration maxRecentAge, + + /// The duration between updates of the UI. + @Default(Duration(seconds: 5)) Duration pageUpdatePeriod, + + /// A scaling factor for order rows. Can be customized in order to achieve + /// more or less dense displays (for example, when the display is farther + /// away from the customers than expected). + @Default(100) double targetRowHeight, + }) = _QueueSettingsState; + + factory QueueSettingsState.fromJson(Map json) => + _$QueueSettingsStateFromJson(json); + + const QueueSettingsState._(); + + static final Logger _logger = Logger('$QueueSettingsState'); + + static const String sharedPrefsKey = 'queue_settings'; + + static QueueSettingsState load(SharedPreferencesRepository repo) { + final jsonString = repo.getString(sharedPrefsKey); + if (jsonString != null) { + try { + return QueueSettingsState.fromJson( + jsonDecode(jsonString) as Map, + ); + } catch (e, s) { + _logger.warning('Failed to load settings', e, s); + } + } + return const QueueSettingsState(); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_settings.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_settings.freezed.dart new file mode 100644 index 0000000..cd1d01d --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_settings.freezed.dart @@ -0,0 +1,330 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'queue_settings.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$QueueSettingsState { + +/// If set to `true`, this screen will only show the first page and will +/// never paginate. +/// +/// If set to `false`, this screen works normally except it ignores +/// the first [topOrdersCount] of orders. + bool get isTopScreen;/// Asks the screen to ignore the first N orders (because, presumably, +/// they are shown on the top screen (see [isTopScreen]). + int get topOrdersCount;/// The shard number of this device. + int get shardNumber;/// The total number of shards in play. + int get shardTotal;/// The maximum age of a completed order, after which the order will not +/// be shown anymore. + Duration get maxShowAge;/// The duration for which an order is considered "recent". The UI may +/// choose to highlight such orders. + Duration get maxRecentAge;/// The duration between updates of the UI. + Duration get pageUpdatePeriod;/// A scaling factor for order rows. Can be customized in order to achieve +/// more or less dense displays (for example, when the display is farther +/// away from the customers than expected). + double get targetRowHeight; +/// Create a copy of QueueSettingsState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$QueueSettingsStateCopyWith get copyWith => _$QueueSettingsStateCopyWithImpl(this as QueueSettingsState, _$identity); + + /// Serializes this QueueSettingsState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is QueueSettingsState&&(identical(other.isTopScreen, isTopScreen) || other.isTopScreen == isTopScreen)&&(identical(other.topOrdersCount, topOrdersCount) || other.topOrdersCount == topOrdersCount)&&(identical(other.shardNumber, shardNumber) || other.shardNumber == shardNumber)&&(identical(other.shardTotal, shardTotal) || other.shardTotal == shardTotal)&&(identical(other.maxShowAge, maxShowAge) || other.maxShowAge == maxShowAge)&&(identical(other.maxRecentAge, maxRecentAge) || other.maxRecentAge == maxRecentAge)&&(identical(other.pageUpdatePeriod, pageUpdatePeriod) || other.pageUpdatePeriod == pageUpdatePeriod)&&(identical(other.targetRowHeight, targetRowHeight) || other.targetRowHeight == targetRowHeight)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,isTopScreen,topOrdersCount,shardNumber,shardTotal,maxShowAge,maxRecentAge,pageUpdatePeriod,targetRowHeight); + +@override +String toString() { + return 'QueueSettingsState(isTopScreen: $isTopScreen, topOrdersCount: $topOrdersCount, shardNumber: $shardNumber, shardTotal: $shardTotal, maxShowAge: $maxShowAge, maxRecentAge: $maxRecentAge, pageUpdatePeriod: $pageUpdatePeriod, targetRowHeight: $targetRowHeight)'; +} + + +} + +/// @nodoc +abstract mixin class $QueueSettingsStateCopyWith<$Res> { + factory $QueueSettingsStateCopyWith(QueueSettingsState value, $Res Function(QueueSettingsState) _then) = _$QueueSettingsStateCopyWithImpl; +@useResult +$Res call({ + bool isTopScreen, int topOrdersCount, int shardNumber, int shardTotal, Duration maxShowAge, Duration maxRecentAge, Duration pageUpdatePeriod, double targetRowHeight +}); + + + + +} +/// @nodoc +class _$QueueSettingsStateCopyWithImpl<$Res> + implements $QueueSettingsStateCopyWith<$Res> { + _$QueueSettingsStateCopyWithImpl(this._self, this._then); + + final QueueSettingsState _self; + final $Res Function(QueueSettingsState) _then; + +/// Create a copy of QueueSettingsState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? isTopScreen = null,Object? topOrdersCount = null,Object? shardNumber = null,Object? shardTotal = null,Object? maxShowAge = null,Object? maxRecentAge = null,Object? pageUpdatePeriod = null,Object? targetRowHeight = null,}) { + return _then(_self.copyWith( +isTopScreen: null == isTopScreen ? _self.isTopScreen : isTopScreen // ignore: cast_nullable_to_non_nullable +as bool,topOrdersCount: null == topOrdersCount ? _self.topOrdersCount : topOrdersCount // ignore: cast_nullable_to_non_nullable +as int,shardNumber: null == shardNumber ? _self.shardNumber : shardNumber // ignore: cast_nullable_to_non_nullable +as int,shardTotal: null == shardTotal ? _self.shardTotal : shardTotal // ignore: cast_nullable_to_non_nullable +as int,maxShowAge: null == maxShowAge ? _self.maxShowAge : maxShowAge // ignore: cast_nullable_to_non_nullable +as Duration,maxRecentAge: null == maxRecentAge ? _self.maxRecentAge : maxRecentAge // ignore: cast_nullable_to_non_nullable +as Duration,pageUpdatePeriod: null == pageUpdatePeriod ? _self.pageUpdatePeriod : pageUpdatePeriod // ignore: cast_nullable_to_non_nullable +as Duration,targetRowHeight: null == targetRowHeight ? _self.targetRowHeight : targetRowHeight // ignore: cast_nullable_to_non_nullable +as double, + )); +} + +} + + +/// Adds pattern-matching-related methods to [QueueSettingsState]. +extension QueueSettingsStatePatterns on QueueSettingsState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _QueueSettingsState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _QueueSettingsState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _QueueSettingsState value) $default,){ +final _that = this; +switch (_that) { +case _QueueSettingsState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _QueueSettingsState value)? $default,){ +final _that = this; +switch (_that) { +case _QueueSettingsState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool isTopScreen, int topOrdersCount, int shardNumber, int shardTotal, Duration maxShowAge, Duration maxRecentAge, Duration pageUpdatePeriod, double targetRowHeight)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _QueueSettingsState() when $default != null: +return $default(_that.isTopScreen,_that.topOrdersCount,_that.shardNumber,_that.shardTotal,_that.maxShowAge,_that.maxRecentAge,_that.pageUpdatePeriod,_that.targetRowHeight);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool isTopScreen, int topOrdersCount, int shardNumber, int shardTotal, Duration maxShowAge, Duration maxRecentAge, Duration pageUpdatePeriod, double targetRowHeight) $default,) {final _that = this; +switch (_that) { +case _QueueSettingsState(): +return $default(_that.isTopScreen,_that.topOrdersCount,_that.shardNumber,_that.shardTotal,_that.maxShowAge,_that.maxRecentAge,_that.pageUpdatePeriod,_that.targetRowHeight);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isTopScreen, int topOrdersCount, int shardNumber, int shardTotal, Duration maxShowAge, Duration maxRecentAge, Duration pageUpdatePeriod, double targetRowHeight)? $default,) {final _that = this; +switch (_that) { +case _QueueSettingsState() when $default != null: +return $default(_that.isTopScreen,_that.topOrdersCount,_that.shardNumber,_that.shardTotal,_that.maxShowAge,_that.maxRecentAge,_that.pageUpdatePeriod,_that.targetRowHeight);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _QueueSettingsState extends QueueSettingsState { + const _QueueSettingsState({this.isTopScreen = false, this.topOrdersCount = 0, this.shardNumber = 1, this.shardTotal = 1, this.maxShowAge = const Duration(minutes: 15), this.maxRecentAge = const Duration(minutes: 5), this.pageUpdatePeriod = const Duration(seconds: 5), this.targetRowHeight = 100}): super._(); + factory _QueueSettingsState.fromJson(Map json) => _$QueueSettingsStateFromJson(json); + +/// If set to `true`, this screen will only show the first page and will +/// never paginate. +/// +/// If set to `false`, this screen works normally except it ignores +/// the first [topOrdersCount] of orders. +@override@JsonKey() final bool isTopScreen; +/// Asks the screen to ignore the first N orders (because, presumably, +/// they are shown on the top screen (see [isTopScreen]). +@override@JsonKey() final int topOrdersCount; +/// The shard number of this device. +@override@JsonKey() final int shardNumber; +/// The total number of shards in play. +@override@JsonKey() final int shardTotal; +/// The maximum age of a completed order, after which the order will not +/// be shown anymore. +@override@JsonKey() final Duration maxShowAge; +/// The duration for which an order is considered "recent". The UI may +/// choose to highlight such orders. +@override@JsonKey() final Duration maxRecentAge; +/// The duration between updates of the UI. +@override@JsonKey() final Duration pageUpdatePeriod; +/// A scaling factor for order rows. Can be customized in order to achieve +/// more or less dense displays (for example, when the display is farther +/// away from the customers than expected). +@override@JsonKey() final double targetRowHeight; + +/// Create a copy of QueueSettingsState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$QueueSettingsStateCopyWith<_QueueSettingsState> get copyWith => __$QueueSettingsStateCopyWithImpl<_QueueSettingsState>(this, _$identity); + +@override +Map toJson() { + return _$QueueSettingsStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _QueueSettingsState&&(identical(other.isTopScreen, isTopScreen) || other.isTopScreen == isTopScreen)&&(identical(other.topOrdersCount, topOrdersCount) || other.topOrdersCount == topOrdersCount)&&(identical(other.shardNumber, shardNumber) || other.shardNumber == shardNumber)&&(identical(other.shardTotal, shardTotal) || other.shardTotal == shardTotal)&&(identical(other.maxShowAge, maxShowAge) || other.maxShowAge == maxShowAge)&&(identical(other.maxRecentAge, maxRecentAge) || other.maxRecentAge == maxRecentAge)&&(identical(other.pageUpdatePeriod, pageUpdatePeriod) || other.pageUpdatePeriod == pageUpdatePeriod)&&(identical(other.targetRowHeight, targetRowHeight) || other.targetRowHeight == targetRowHeight)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,isTopScreen,topOrdersCount,shardNumber,shardTotal,maxShowAge,maxRecentAge,pageUpdatePeriod,targetRowHeight); + +@override +String toString() { + return 'QueueSettingsState(isTopScreen: $isTopScreen, topOrdersCount: $topOrdersCount, shardNumber: $shardNumber, shardTotal: $shardTotal, maxShowAge: $maxShowAge, maxRecentAge: $maxRecentAge, pageUpdatePeriod: $pageUpdatePeriod, targetRowHeight: $targetRowHeight)'; +} + + +} + +/// @nodoc +abstract mixin class _$QueueSettingsStateCopyWith<$Res> implements $QueueSettingsStateCopyWith<$Res> { + factory _$QueueSettingsStateCopyWith(_QueueSettingsState value, $Res Function(_QueueSettingsState) _then) = __$QueueSettingsStateCopyWithImpl; +@override @useResult +$Res call({ + bool isTopScreen, int topOrdersCount, int shardNumber, int shardTotal, Duration maxShowAge, Duration maxRecentAge, Duration pageUpdatePeriod, double targetRowHeight +}); + + + + +} +/// @nodoc +class __$QueueSettingsStateCopyWithImpl<$Res> + implements _$QueueSettingsStateCopyWith<$Res> { + __$QueueSettingsStateCopyWithImpl(this._self, this._then); + + final _QueueSettingsState _self; + final $Res Function(_QueueSettingsState) _then; + +/// Create a copy of QueueSettingsState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? isTopScreen = null,Object? topOrdersCount = null,Object? shardNumber = null,Object? shardTotal = null,Object? maxShowAge = null,Object? maxRecentAge = null,Object? pageUpdatePeriod = null,Object? targetRowHeight = null,}) { + return _then(_QueueSettingsState( +isTopScreen: null == isTopScreen ? _self.isTopScreen : isTopScreen // ignore: cast_nullable_to_non_nullable +as bool,topOrdersCount: null == topOrdersCount ? _self.topOrdersCount : topOrdersCount // ignore: cast_nullable_to_non_nullable +as int,shardNumber: null == shardNumber ? _self.shardNumber : shardNumber // ignore: cast_nullable_to_non_nullable +as int,shardTotal: null == shardTotal ? _self.shardTotal : shardTotal // ignore: cast_nullable_to_non_nullable +as int,maxShowAge: null == maxShowAge ? _self.maxShowAge : maxShowAge // ignore: cast_nullable_to_non_nullable +as Duration,maxRecentAge: null == maxRecentAge ? _self.maxRecentAge : maxRecentAge // ignore: cast_nullable_to_non_nullable +as Duration,pageUpdatePeriod: null == pageUpdatePeriod ? _self.pageUpdatePeriod : pageUpdatePeriod // ignore: cast_nullable_to_non_nullable +as Duration,targetRowHeight: null == targetRowHeight ? _self.targetRowHeight : targetRowHeight // ignore: cast_nullable_to_non_nullable +as double, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_settings.g.dart b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_settings.g.dart new file mode 100644 index 0000000..363368e --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/home/queue_settings.g.dart @@ -0,0 +1,41 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'queue_settings.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_QueueSettingsState _$QueueSettingsStateFromJson(Map json) => + _QueueSettingsState( + isTopScreen: json['isTopScreen'] as bool? ?? false, + topOrdersCount: (json['topOrdersCount'] as num?)?.toInt() ?? 0, + shardNumber: (json['shardNumber'] as num?)?.toInt() ?? 1, + shardTotal: (json['shardTotal'] as num?)?.toInt() ?? 1, + maxShowAge: json['maxShowAge'] == null + ? const Duration(minutes: 15) + : Duration(microseconds: (json['maxShowAge'] as num).toInt()), + maxRecentAge: json['maxRecentAge'] == null + ? const Duration(minutes: 5) + : Duration(microseconds: (json['maxRecentAge'] as num).toInt()), + pageUpdatePeriod: json['pageUpdatePeriod'] == null + ? const Duration(seconds: 5) + : Duration(microseconds: (json['pageUpdatePeriod'] as num).toInt()), + targetRowHeight: (json['targetRowHeight'] as num?)?.toDouble() ?? 100, + ); + +Map _$QueueSettingsStateToJson(_QueueSettingsState instance) => + { + 'isTopScreen': instance.isTopScreen, + 'topOrdersCount': instance.topOrdersCount, + 'shardNumber': instance.shardNumber, + 'shardTotal': instance.shardTotal, + 'maxShowAge': instance.maxShowAge.inMicroseconds, + 'maxRecentAge': instance.maxRecentAge.inMicroseconds, + 'pageUpdatePeriod': instance.pageUpdatePeriod.inMicroseconds, + 'targetRowHeight': instance.targetRowHeight, + }; diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/queue.dart b/genlatte/genlatte-ui/lib/src/screens/queue/queue.dart new file mode 100644 index 0000000..5dbfdbd --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/queue.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'home/queue_home.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/util/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/queue/util/CONTEXT.md new file mode 100644 index 0000000..87fb928 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/util/CONTEXT.md @@ -0,0 +1,13 @@ +# Queue Util + +**Purpose:** +Provides pure utility helper abstractions specific to the `queue` domain, specifically around logging formatting. + +**Detailed File Overviews:** + +- `metadata_describe.dart`: + - **Description**: An extension on `LatteOrderMetadata`. + - **Core Logic**: Exposes a `.describe()` method that formats a debugging string, appending a `-completed` suffix if the `completionTime` field is non-null. Widely used inside `queue_home_bloc` logging. + +**Dependencies/Relationships:** +- Imposed via Extension on `genlatte_data`'s `LatteOrderMetadata` class. diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/util/metadata_describe.dart b/genlatte/genlatte-ui/lib/src/screens/queue/util/metadata_describe.dart new file mode 100644 index 0000000..040dd95 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/util/metadata_describe.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte_data/models.dart'; + +extension MetadataDescribe on LatteOrderMetadata { + String describe() { + final buf = StringBuffer('<')..write(id); + if (completionTime != null) { + buf.write('-completed'); + } + buf.write('>'); + return buf.toString(); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/widgets/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/CONTEXT.md new file mode 100644 index 0000000..e51716c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/CONTEXT.md @@ -0,0 +1,25 @@ +# Queue Widgets + +**Purpose:** +Provides complex UI components specifically built for the `queue` display screen. This includes the massive list view and its nested child items, as well as a specialized tool for configuring the display board's hardware installation properties. + +**Detailed File Overviews:** + +- `debug_overlay.dart`: + - **Description**: Defines `OrderBoardDebugOverlay`. + - **Core Logic**: An intense settings panel containing complex form inputs allowing field technicians to change hardware behavior without rebuilding the app. It modifies values managed by `QueueHomeBloc` like "Target row height", active Shards to compute modulo routing mathematics, and "Page update periods". It also contains fake data generator tools for offline mock testing. + +- `order_list.dart`: + - **Description**: The core container `OrderList` component mapping to multiple pages of orders. + - **Core Logic**: Employs `AnimationController` and `Timer` mechanics synchronized perfectly to the nearest clock-synchronous interval to compute autonomous Page flip transitions. It pre-caches Latte images exactly `1` second before flipping pages to avoid image flash pops. + +- `order_list_item.dart`: + - **Description**: An individual row in the queue representing a single customer order. + - **Core Logic**: Renders the specific completion status, customer name, and latte image. Handles complex staggering animation delays based on its index sequence (`positionInList`) during page flips. + +- `status_icon.dart`: + - **Description**: Converts the semantic `LatteOrderStatus` enum to animated icon combinations. + +**Dependencies/Relationships:** +- Used heavily by `queue/home/queue_home_view.dart`. +- Uses `shadcn_flutter` for core primitive forms. diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/widgets/debug_overlay.dart b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/debug_overlay.dart new file mode 100644 index 0000000..a0908eb --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/debug_overlay.dart @@ -0,0 +1,743 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/queue/home/fake_repositories.dart'; +import 'package:genlatte/src/screens/queue/home/queue_home.dart'; +import 'package:genlatte/src/screens/queue/util/metadata_describe.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:logging/logging.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +class OrderBoardDebugOverlay extends StatefulWidget { + const OrderBoardDebugOverlay({ + required this.bloc, + required this.ordersRepository, + required this.metadataRepository, + super.key, + }); + + final QueueHomeBloc bloc; + + /// The provided fake orders repository. + final FakeLatteOrdersRepository? ordersRepository; + + /// The provided fake metadata repository. + final FakeLatteOrdersMetadataRepository? metadataRepository; + + @override + State createState() => _OrderBoardDebugOverlayState(); +} + +class _CheckboxField extends StatefulWidget { + const _CheckboxField({ + required this.formKey, + required this.label, + required this.initialValue, + }); + + final FormKey formKey; + final String label; + final bool initialValue; + + @override + State<_CheckboxField> createState() => _CheckboxFieldState(); +} + +class _CheckboxFieldState extends State<_CheckboxField> { + late bool _state = widget.initialValue; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + FormField( + key: widget.formKey, + label: Text(widget.label), + child: Switch( + value: _state, + onChanged: (value) { + setState(() { + _state = value; + }); + }, + ), + ), + ], + ); + } +} + +class _NumberFieldWithButtons extends StatelessWidget { + const _NumberFieldWithButtons({ + required this.formKey, + required this.label, + required this.controller, + required this.step, + required this.fallbackValue, + this.trailingLabel, + }); + + final FormKey formKey; + final String label; + final String? trailingLabel; + final TextEditingController controller; + final double step; + final double fallbackValue; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + FormField( + key: formKey, + label: Text(label), + trailingLabel: trailingLabel != null ? Text(trailingLabel!) : null, + child: TextField( + controller: controller, + ), + ), + Row( + spacing: 8, + children: [ + Button( + onPressed: () { + final currentString = Form.of(context).getValue(formKey); + final current = double.tryParse(currentString ?? ''); + if (current != null) { + final val = current - step; + controller.text = val == val.toInt() + ? val.toInt().toString() + : val.toString(); + } else { + controller.text = fallbackValue == fallbackValue.toInt() + ? fallbackValue.toInt().toString() + : fallbackValue.toString(); + } + }, + style: const ButtonStyle.outlineIcon(), + child: const Text('-'), + ), + Button( + onPressed: () { + final currentString = Form.of(context).getValue(formKey); + final current = double.tryParse(currentString ?? ''); + if (current != null) { + final val = current + step; + controller.text = val == val.toInt() + ? val.toInt().toString() + : val.toString(); + } else { + controller.text = fallbackValue == fallbackValue.toInt() + ? fallbackValue.toInt().toString() + : fallbackValue.toString(); + } + }, + style: const ButtonStyle.outlineIcon(), + child: const Text('+'), + ), + ], + ), + ], + ); + } +} + +class _OrderBoardDebugOverlayState extends State { + static final Logger _logger = Logger('$_OrderBoardDebugOverlayState'); + + static int _orderIdCounter = 1; + + final _isTopScreenKey = const FormKey('is_top_screen'); + + final _topOrdersCountKey = const FormKey('top_orders_count'); + + final _maxRecentAgeKey = const FormKey('max_recent_age'); + + final _maxShowAgeKey = const FormKey('max_show_age'); + + final _shardNumberKey = const FormKey('shard_number'); + + final _shardTotalKey = const FormKey('shard_total'); + + final _pageUpdatePeriodKey = const FormKey('page_update_period'); + + final _targetRowHeightKey = const FormKey('target_row_height'); + + late final _topOrdersCountController = TextEditingController( + text: widget.bloc.state.settings.topOrdersCount.toString(), + ); + + late final _maxRecentAgeController = TextEditingController( + text: widget.bloc.state.settings.maxRecentAge.asMinutesString, + ); + + late final _maxShowAgeController = TextEditingController( + text: widget.bloc.state.settings.maxShowAge.asMinutesString, + ); + + late final _shardNumberController = TextEditingController( + text: widget.bloc.state.settings.shardNumber.toString(), + ); + + late final _shardTotalController = TextEditingController( + text: widget.bloc.state.settings.shardTotal.toString(), + ); + + late final _pageUpdatePeriodController = TextEditingController( + text: widget.bloc.state.settings.pageUpdatePeriod.asSecondsString, + ); + + late final _targetRowHeightController = TextEditingController( + text: widget.bloc.state.settings.targetRowHeight.toString(), + ); + + bool get _hasFakes => + widget.ordersRepository != null && widget.metadataRepository != null; + + @override + Widget build(BuildContext context) { + final settings = widget.bloc.state.settings; + + return ColoredBox( + color: AppColors.chevronYellow, + child: DefaultTextStyle( + style: const TextStyle(color: AppColors.black), + child: Padding( + padding: const EdgeInsetsGeometry.all(16), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + spacing: 8, + children: [ + const Text('SETUP'), + Form( + onSubmit: (context, values) { + final isTopScreen = _isTopScreenKey[values]; + final topOrdersCount = int.tryParse( + _topOrdersCountKey[values] ?? '', + ); + + if (isTopScreen != null && topOrdersCount != null) { + widget.bloc.add( + SetTopSettings( + isTopScreen: isTopScreen, + topOrdersCount: topOrdersCount, + ), + ); + } else { + showToast( + context: context, + builder: (context, overlay) { + return SurfaceCard( + child: Basic( + title: const Text('Invalid top settings'), + subtitle: Text( + 'Top screen: ${_isTopScreenKey[values]}\n' + 'Top orders count: ${_topOrdersCountKey[values]}', + ), + ), + ); + }, + ); + } + }, + child: Column( + crossAxisAlignment: .end, + spacing: 8, + children: [ + Row( + crossAxisAlignment: .start, + spacing: 8, + children: [ + _CheckboxField( + formKey: _isTopScreenKey, + label: 'is top', + initialValue: widget.bloc.state.settings.isTopScreen, + ), + const SizedBox( + width: 16, + ), + _NumberFieldWithButtons( + formKey: _topOrdersCountKey, + label: 'top orders count', + controller: _topOrdersCountController, + step: 1, + fallbackValue: settings.topOrdersCount.toDouble(), + ), + ], + ), + FormErrorBuilder( + builder: (context, errors, child) => Button( + enableFeedback: true, + style: const ButtonStyle.outline(), + onPressed: errors.isEmpty + ? () => context.submitForm() + : null, + child: const Text('Save'), + ), + ), + ], + ), + ), + Form( + onSubmit: (context, values) { + final shardNumber = int.tryParse( + _shardNumberKey[values] ?? '', + ); + final shardTotal = int.tryParse(_shardTotalKey[values] ?? ''); + + if (shardNumber != null && shardTotal != null) { + widget.bloc.add(SetShard(shardNumber, shardTotal)); + } else { + showToast( + context: context, + builder: (context, overlay) { + return SurfaceCard( + child: Basic( + title: const Text('Invalid shard setup'), + subtitle: Text( + 'Shard #: ${_shardNumberKey[values]}\n' + 'Shard total: ${_shardTotalKey[values]}', + ), + ), + ); + }, + ); + } + }, + child: Column( + crossAxisAlignment: .end, + spacing: 8, + children: [ + Row( + crossAxisAlignment: .start, + spacing: 8, + children: [ + _NumberFieldWithButtons( + formKey: _shardNumberKey, + label: 'shard #', + controller: _shardNumberController, + step: 1, + fallbackValue: settings.shardNumber.toDouble(), + ), + const Padding( + padding: EdgeInsets.only(top: 32), + child: Text(' of '), + ), + _NumberFieldWithButtons( + formKey: _shardTotalKey, + label: 'shards total', + controller: _shardTotalController, + step: 1, + fallbackValue: settings.shardTotal.toDouble(), + ), + ], + ), + FormErrorBuilder( + builder: (context, errors, child) => Button( + enableFeedback: true, + style: const ButtonStyle.outline(), + onPressed: errors.isEmpty + ? () => context.submitForm() + : null, + child: const Text('Save'), + ), + ), + ], + ), + ), + Form( + onSubmit: (context, values) { + final pageUpdatePeriod = double.tryParse( + _pageUpdatePeriodKey[values] ?? '', + ); + + if (pageUpdatePeriod != null) { + widget.bloc.add( + SetPageUpdatePeriod( + Duration( + milliseconds: + (pageUpdatePeriod * + Duration.millisecondsPerSecond) + .round(), + ), + ), + ); + } else { + showToast( + context: context, + builder: (context, overlay) { + return SurfaceCard( + child: Basic( + title: const Text( + 'Invalid page update period setup', + ), + subtitle: Text( + 'Page update period: ' + '${_pageUpdatePeriodKey[values]}', + ), + ), + ); + }, + ); + } + }, + child: Column( + crossAxisAlignment: .end, + spacing: 8, + children: [ + Row( + crossAxisAlignment: .start, + spacing: 8, + children: [ + _NumberFieldWithButtons( + formKey: _pageUpdatePeriodKey, + label: 'Page update period', + trailingLabel: 'in seconds', + controller: _pageUpdatePeriodController, + step: 1, + fallbackValue: double.parse( + settings.pageUpdatePeriod.asSecondsString, + ), + ), + ], + ), + FormErrorBuilder( + builder: (context, errors, child) => Button( + enableFeedback: true, + style: const ButtonStyle.outline(), + onPressed: errors.isEmpty + ? () => context.submitForm() + : null, + child: const Text('Save'), + ), + ), + ], + ), + ), + Form( + onSubmit: (context, values) { + final targetRowHeight = double.tryParse( + _targetRowHeightKey[values] ?? '', + ); + + if (targetRowHeight != null) { + widget.bloc.add(SetTargetRowHeight(targetRowHeight)); + } else { + showToast( + context: context, + builder: (context, overlay) { + return SurfaceCard( + child: Basic( + title: const Text('Invalid target row height'), + subtitle: Text( + 'Height: ${_targetRowHeightKey[values]}', + ), + ), + ); + }, + ); + } + }, + child: Column( + crossAxisAlignment: .end, + spacing: 8, + children: [ + Row( + crossAxisAlignment: .start, + children: [ + _NumberFieldWithButtons( + formKey: _targetRowHeightKey, + label: 'Target row height', + trailingLabel: 'in pixels', + controller: _targetRowHeightController, + step: 10, + fallbackValue: settings.targetRowHeight, + ), + ], + ), + FormErrorBuilder( + builder: (context, errors, child) => Button( + enableFeedback: true, + style: const ButtonStyle.outline(), + onPressed: errors.isEmpty + ? () => context.submitForm() + : null, + child: const Text('Save'), + ), + ), + ], + ), + ), + + Form( + onSubmit: (context, values) { + final maxRecentAge = double.tryParse( + _maxRecentAgeKey[values] ?? '', + ); + final maxShowAge = double.tryParse( + _maxShowAgeKey[values] ?? '', + ); + + if (maxRecentAge != null && maxShowAge != null) { + widget.bloc.add( + SetRecency( + Duration( + seconds: (maxRecentAge * Duration.secondsPerMinute) + .round(), + ), + Duration( + seconds: (maxShowAge * Duration.secondsPerMinute) + .round(), + ), + ), + ); + } else { + showToast( + context: context, + builder: (context, overlay) { + return SurfaceCard( + child: Basic( + title: const Text('Invalid recency setup'), + subtitle: Text( + 'Max recent age: ${_maxRecentAgeKey[values]}\n' + 'Max show age: ${_maxShowAgeKey[values]}', + ), + ), + ); + }, + ); + } + }, + child: Column( + crossAxisAlignment: .end, + spacing: 8, + children: [ + Row( + crossAxisAlignment: .start, + spacing: 8, + children: [ + _NumberFieldWithButtons( + formKey: _maxRecentAgeKey, + label: 'Max recent age', + trailingLabel: 'in minutes', + controller: _maxRecentAgeController, + step: 1, + fallbackValue: double.parse( + settings.maxRecentAge.asMinutesString, + ), + ), + _NumberFieldWithButtons( + formKey: _maxShowAgeKey, + label: 'Max show age', + trailingLabel: 'in minutes', + controller: _maxShowAgeController, + step: 1, + fallbackValue: double.parse( + settings.maxShowAge.asMinutesString, + ), + ), + ], + ), + FormErrorBuilder( + builder: (context, errors, child) => Button( + enableFeedback: true, + style: const ButtonStyle.outline(), + onPressed: errors.isEmpty + ? () => context.submitForm() + : null, + child: const Text('Save'), + ), + ), + ], + ), + ), + + const Text('DEBUG'), + + Button( + enableFeedback: true, + style: const ButtonStyle.outline(), + onPressed: _hasFakes + ? () async { + _logger.fine( + 'Add order pressed', + ); + final id = _orderIdCounter++; + + await widget.ordersRepository!.setItem( + LatteOrder( + id: '$id', + name: const [ + 'Alice', + 'Bob', + 'Clarence', + 'Daphne', + 'Eve', + 'Frank', + 'Grace', + 'Heidi', + 'Ivan', + 'Judy', + 'Karl', + 'Leo', + 'Mallory', + 'Nina', + ][id % 14], + milk: 'nope', + sweetener: 'yes', + happyPlace: 'home', + ), + ); + await widget.metadataRepository!.setItem( + LatteOrderMetadata( + id: '$id', + orderNumber: id, + isNameApproved: true, + isHappyPlaceApproved: true, + isImageApproved: true, + imageBatchId: 'image-batch-$id', + imageUrl: null, + status: .submitted, + orderSubmittedTime: DateTime.now(), + ), + ); + } + : null, + + child: const Text('Add order'), + ), + Button( + enableFeedback: true, + style: const ButtonStyle.outline(), + onPressed: _hasFakes + ? () async { + _logger.fine('Claim random order pressed'); + + final allItems = + (await widget.metadataRepository!.getItems()) + .where( + (o) => + o.status != .completed && + o.status != .inProgress && + o.status != .archived, + ) + .toList(); + if (allItems.isEmpty) return; + + final random = Random(); + final randomItem = + allItems[random.nextInt(allItems.length)]; + _logger.fine( + 'Picked: ${randomItem.describe()}', + ); + + final modified = randomItem.copyWith( + status: .inProgress, + ); + _logger.fine('Modified: ${modified.describe()}'); + + await widget.metadataRepository!.setItem(modified); + } + : null, + child: const Text('Claim random order'), + ), + Button( + enableFeedback: true, + style: const ButtonStyle.outline(), + onPressed: _hasFakes + ? () async { + _logger.fine('Complete random order pressed'); + + final allItems = + (await widget.metadataRepository!.getItems()) + .where((o) => o.status == .inProgress) + .toList(); + if (allItems.isEmpty) return; + + final random = Random(); + final randomItem = + allItems[random.nextInt(allItems.length)]; + _logger.fine( + 'Picked: ${randomItem.describe()}', + ); + + final modified = randomItem.copyWith( + completionTime: DateTime.now(), + status: .completed, + ); + _logger.fine('Modified: ${modified.describe()}'); + + await widget.metadataRepository!.setItem(modified); + } + : null, + child: const Text('Complete random order'), + ), + Button( + enableFeedback: true, + style: const ButtonStyle.outline(), + onPressed: _hasFakes + ? () async { + _logger.fine('Archive random order pressed'); + + var allItems = + (await widget.metadataRepository!.getItems()) + .where((o) => o.status == .completed) + .toList(); + if (allItems.isEmpty) { + allItems = + (await widget.metadataRepository!.getItems()) + .where((o) => o.status != .archived) + .toList(); + } + if (allItems.isEmpty) return; + + final random = Random(); + final randomItem = + allItems[random.nextInt(allItems.length)]; + _logger.fine('Picked: ${randomItem.describe()}'); + + final modified = randomItem.copyWith( + completionTime: DateTime.now().subtract( + const Duration(days: 14), + ), + status: .archived, + ); + _logger.fine('Modified: ${modified.describe()}'); + + await widget.metadataRepository!.setItem(modified); + } + : null, + child: const Text('Archive random order'), + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _maxRecentAgeController.dispose(); + _maxShowAgeController.dispose(); + _shardNumberController.dispose(); + _shardTotalController.dispose(); + _pageUpdatePeriodController.dispose(); + _targetRowHeightController.dispose(); + super.dispose(); + } +} + +extension on Duration { + String get asMinutesString => (inSeconds / 60).toString(); + String get asSecondsString => (inMilliseconds / 1000).toString(); +} diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/widgets/order_list.dart b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/order_list.dart new file mode 100644 index 0000000..fac394f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/order_list.dart @@ -0,0 +1,426 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/queue/home/queue_home.dart'; +import 'package:genlatte/src/screens/queue/widgets/order_list_item.dart'; +import 'package:genlatte/src/widgets/triple_tap.dart'; +import 'package:genlatte/src/widgets/typography.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +class OrderList extends StatefulWidget { + const OrderList({ + required this.pages, + required this.onSlotsCounted, + required this.maxRecentAge, + required this.targetRowHeight, + required this.noOrdersInAnyShard, + required this.pageUpdatePeriod, + super.key, + }); + + /// Image used for latte orders whose images aren't approved (yet). + /// + /// Follows the example of + /// https://github.com/GoogleCloudDemos/gcdemos-26-int-dd-latteart/pull/197. + static const fallbackImageUrl = + 'https://firebasestorage.googleapis.com/v0/b/gcdemos-26-int-dd-latteart.firebasestorage.app/o/latteImages%2FfallbackImage%2Ficon_flutter.png?alt=media&token=06a3ac7e-7929-4507-8105-9eddb4445892'; + + final List pages; + + final void Function(int) onSlotsCounted; + + final Duration maxRecentAge; + + final Duration pageUpdatePeriod; + + final double targetRowHeight; + + final bool noOrdersInAnyShard; + + @override + State createState() => _OrderListState(); +} + +class _OrderListState extends State + with SingleTickerProviderStateMixin { + static final Logger _logger = Logger('$_OrderListState'); + + /// The amount of time before every tick when we preload images. + static const Duration _preloadDuration = Duration(seconds: 1); + + /// The length of the transition of each row in the list. + static const Duration _rowTransitionDuration = Duration(milliseconds: 250); + + late final _transitionController = AnimationController(vsync: this); + + /// The tick that fires every N seconds since midnight. This ensures that + /// multiple displays all update at the same time. + Timer? _tickTimer; + + /// To prevent ugly image loading flashes, we preload images + /// a small amount of time ([_preloadDuration]) before every tick. + Timer? _preloadTimer; + + int currentPageIndex = 0; + + List currentPages = const []; + + List nextPages = const []; + + @override + Widget build(BuildContext context) { + return _Padding( + child: Column( + crossAxisAlignment: .start, + children: [ + Row( + children: [ + TripleTapDetector( + semanticLabel: 'Screen headline', + semanticHint: 'Triple tap to log out', + onPressed: () => GetIt.I().signOut(), + child: const Text('Order progress').h3, + ), + const Spacer(), + _PageIndicator( + currentPageIndex: currentPageIndex, + pageOrderCounts: currentPages + .map((p) => p.orders.nonNulls.length) + .toList(growable: false), + ), + ], + ), + const SizedBox(height: 14), + const ColoredBox( + color: AppColors.placeholderGrey, + child: SizedBox(width: double.infinity, height: 6), + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + // TODO(filiph): add a heuristic that selects the best height + // according to orientation + final slotsAvailable = + (constraints.maxHeight / widget.targetRowHeight).floor(); + widget.onSlotsCounted(slotsAvailable); + + if (currentPageIndex >= currentPages.length) { + if (widget.noOrdersInAnyShard) { + return Center( + child: Column( + mainAxisAlignment: .center, + spacing: 16, + children: [ + const Text.rich( + TextSpan( + children: [ + TextSpan(text: 'Yes, you '), + TextSpan( + text: 'need', + style: TextStyle(decoration: .underline), + ), + TextSpan(text: ' coffee.'), + ], + ), + ).h2_, + const Text('Waiting for the first order.'), + ], + ), + ); + } else { + return const SizedBox.expand(); + } + } + final page = currentPages[currentPageIndex]; + + return Column( + crossAxisAlignment: .start, + children: [ + for (final (index, item) in page.orders.indexed) + Expanded( + key: Key('position-$index'), + child: OrderListItem( + item, + positionInList: index, + listLength: page.orders.length, + transitionAnimation: _transitionController, + animationDuration: _rowTransitionDuration, + maxRecentAge: widget.maxRecentAge, + addBottomDivider: + item != null && index < page.orders.length - 1, + ), + ), + ], + ); + }, + ), + ), + const ColoredBox( + color: AppColors.placeholderGrey, + child: SizedBox( + width: double.infinity, + height: 6, + ), + ), + ], + ), + ); + } + + @override + void didUpdateWidget(covariant OrderList oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.pages != nextPages) { + _logger.fine('nextPages updated in didUpdateWidget()'); + nextPages = widget.pages; + } + } + + @override + void dispose() { + _tickTimer?.cancel(); + _preloadTimer?.cancel(); + _transitionController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + currentPages = widget.pages; + nextPages = widget.pages; + + _startTransitionAnimation(); + _startTickTimer(); + } + + void _preloadImages() { + if (!mounted) { + _logger.info('_preloadImages() called after widget unmount, skipping'); + return; + } + + // Extract image urls from the orders that are going to be shown next. + // It's still possible for a new order to show up between now and the next + // tick, but the chances are low. + final urls = nextPages + .expand((page) => page.orders) + .nonNulls + .map( + (o) => + o.metadata.isImageApproved == true ? o.metadata.imageUrl : null, + ) + .nonNulls + .followedBy(const [OrderList.fallbackImageUrl]); + + for (final imageUrl in urls) { + precacheImage( + NetworkImage(imageUrl), + context, + onError: (e, s) => _logger.fine('Error when precaching image', e, s), + ).then((_) => _logger.finest('Image preloaded: $imageUrl')).ignore(); + } + + precacheImage( + const AssetImage('assets/latte-background-thumb.png'), + context, + onError: (e, s) => + _logger.fine('Error when precaching latte background image', e, s), + ).then((_) => _logger.finest('Latte background image preloaded')).ignore(); + } + + void _startTickTimer() { + final now = DateTime.now(); + final midnight = DateTime(now.year, now.month, now.day); + final timeInMs = now.difference(midnight).inMilliseconds; + final periodInMs = widget.pageUpdatePeriod.inMilliseconds; + final nextBoundaryInMs = + // Round up. + (timeInMs / periodInMs).ceil() * periodInMs; + final delayMs = nextBoundaryInMs - timeInMs; + final delay = Duration(milliseconds: delayMs); + + _tickTimer?.cancel(); + _tickTimer = Timer(delay, _tick); + + _preloadTimer?.cancel(); + if (delay > _preloadDuration) { + // We have time to schedule preloading, too. + _preloadTimer = Timer(delay - _preloadDuration, _preloadImages); + } + } + + void _startTransitionAnimation() { + if (currentPages.isEmpty) return; + final listLength = currentPages[currentPageIndex].orders.length; + _transitionController.duration = _rowTransitionDuration * listLength; + unawaited(_transitionController.forward(from: 0)); + } + + /// Each tick, we either go to the next page of [currentPages], + /// or (if all pages have been shown), we switch to [nextPages] a start anew. + /// + /// Called periodically via [_tickTimer]. + void _tick() { + if (!mounted) { + _logger.info('_tick() called after widget unmount, skipping'); + return; + } + + final bool hasFinishedWithCurrentPages; + if (currentPages.isEmpty) { + hasFinishedWithCurrentPages = true; + } else { + hasFinishedWithCurrentPages = currentPageIndex >= currentPages.length - 1; + } + + if (hasFinishedWithCurrentPages) { + setState(() { + currentPageIndex = 0; + currentPages = nextPages; + }); + _logger.fine( + 'Switching to new list of pages, ' + 'showing page 1 of ${currentPages.length}', + ); + } else { + setState(() => currentPageIndex += 1); + _logger.fine( + 'Showing page ${currentPageIndex + 1} ' + 'of ${currentPages.length}', + ); + } + + _startTransitionAnimation(); + _startTickTimer(); + } +} + +class _Padding extends StatelessWidget { + const _Padding({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: AppColors.placeholderGrey, width: 16), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(64, 40, 64, 57), + child: child, + ), + ); + } +} + +class _PageIndicator extends StatelessWidget { + const _PageIndicator({ + required this.pageOrderCounts, + required this.currentPageIndex, + }); + + static const int minPages = 3; + + static const double width = 40; + + static const double height = 30; + + final List pageOrderCounts; + + final int currentPageIndex; + + @override + Widget build(BuildContext context) { + if (pageOrderCounts.isEmpty) { + return const SizedBox(width: minPages * width, height: height); + } + + final pageCount = pageOrderCounts.length; + final orderCountMax = pageOrderCounts.fold(0, max); + + return SizedBox( + width: max(pageOrderCounts.length, minPages) * width, + height: height, + child: Stack( + fit: .expand, + children: [ + for (final (index, count) in pageOrderCounts.indexed) + Positioned( + right: (pageCount - index - 1) * width, + width: width, + top: 0, + bottom: 0, + child: _PageIndicatorIcon( + orderCount: count, + orderCountMax: orderCountMax, + width: width, + ), + ), + AnimatedPositioned( + right: (pageCount - currentPageIndex - 1) * width, + width: width, + top: 0, + bottom: 0, + duration: const Duration(milliseconds: 1000), + curve: Curves.easeInOut, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: AppColors.placeholderGrey, width: 2), + ), + ), + ), + ], + ), + ); + } +} + +class _PageIndicatorIcon extends StatelessWidget { + const _PageIndicatorIcon({ + required this.orderCountMax, + required this.width, + required this.orderCount, + }); + + final int orderCount; + + final int orderCountMax; + + final double width; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: .stretch, + spacing: 1, + children: [ + for (var i = 0; i < orderCountMax; i++) + Expanded( + child: ColoredBox( + color: i < orderCount + ? AppColors.placeholderGrey + : AppColors.transparent, + ), + ), + ], + ), + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/widgets/order_list_item.dart b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/order_list_item.dart new file mode 100644 index 0000000..881e7c8 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/order_list_item.dart @@ -0,0 +1,377 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/queue/widgets/order_list.dart'; +import 'package:genlatte/src/screens/queue/widgets/status_icon.dart'; +import 'package:genlatte/src/widgets/latte_image.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:logging/logging.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +class OrderListItem extends StatefulWidget { + const OrderListItem( + this.latte, { + required this.positionInList, + required this.listLength, + required this.transitionAnimation, + required this.animationDuration, + required this.maxRecentAge, + required this.addBottomDivider, + super.key, + }); + + final Latte? latte; + + final int positionInList; + + final int listLength; + + final Animation transitionAnimation; + + final Duration animationDuration; + + final Duration maxRecentAge; + + final bool addBottomDivider; + + @override + State createState() => _OrderListItemState(); +} + +class _OrderListItemState extends State { + static final Logger _log = Logger('$_OrderListItemState'); + + /// Keeps ahold of the latest order we showed so that we can properly + /// transition to whatever is the current one. + Latte? _previous; + + bool _previouslyNeededDivider = false; + + @override + Widget build(BuildContext context) { + final current = widget.latte; + + final animateIn = widget.transitionAnimation.drive( + // Animating from the bottom up (the later the position, the sooner + // we start animating). + // + // The sequence goes something like this, assuming this is order 2 of 4. + // + // 1 ___________________ + // ___/ + // ___/ + // 0 _________/ + // ^ ^ ^ ^ ^ + // Start Order 2 Order 3 Order 4 End + TweenSequence([ + if (widget.positionInList < widget.listLength - 1) + TweenSequenceItem( + tween: ConstantTween(0), + weight: widget.listLength - widget.positionInList - 1, + ), + TweenSequenceItem( + tween: Tween(begin: 0, end: 1), + weight: 1, + ), + if (widget.positionInList > 0) + TweenSequenceItem( + tween: ConstantTween(1), + weight: widget.positionInList.toDouble(), + ), + ]), + ); + late final animateOut = ReverseAnimation(animateIn); + + final now = DateTime.now(); + + Widget widgetForLatte(Latte latte, bool needsDivider) { + final String text; + if (latte.metadata.isImageApproved == true && latte.order.name != null) { + text = latte.order.name!; + } else { + final number = latte.metadata.orderNumber; + if (number != null) { + text = 'Order #$number'; + } else { + text = 'Mysterious order'; + } + } + + final OrderStatus state; + final submittedTime = latte.metadata.orderSubmittedTime; + final completionTime = latte.metadata.completionTime; + final sinceCompletion = completionTime != null + ? now.difference(completionTime) + : null; + if (sinceCompletion != null && sinceCompletion > widget.maxRecentAge) { + state = .completed; + } else if (sinceCompletion != null) { + state = .recentlyCompleted; + } else if (latte.metadata.status == .inProgress) { + state = .atBarista; + } else if (submittedTime != null) { + state = .visible; + } else { + _log.warning( + 'Non-submitted item somehow made it into the queue: $latte', + ); + return const SizedBox(); + } + + return Column( + children: [ + Expanded( + child: _OrderWidget( + text: text, + imageUrl: latte.metadata.isImageApproved == true + ? latte.metadata.imageUrl + : null, + status: state, + ), + ), + ColoredBox( + color: needsDivider + ? AppColors.placeholderGrey + : AppColors.transparent, + child: const SizedBox( + width: double.infinity, + height: 3, + ), + ), + ], + ); + } + + // TODO(filiph): this widget should try to have more stable children + // especially if we're going from same id to same id + return switch ((_previous, current)) { + (null, null) => const SizedBox.expand(), + (final Latte previous, null) => SlideTransition( + position: + Tween( + begin: const Offset(0, -0.1), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animateOut, + curve: Curves.easeOutCubic, + ), + ), + child: FadeTransition( + opacity: CurvedAnimation( + parent: animateOut, + curve: Curves.easeInCubic, + ), + child: widgetForLatte(previous, _previouslyNeededDivider), + ), + ), + (null, final Latte current) => SlideTransition( + position: + Tween( + begin: const Offset(0, 0.1), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animateIn, + curve: Curves.easeOutCubic, + ), + ), + child: FadeTransition( + opacity: animateIn, + child: widgetForLatte(current, widget.addBottomDivider), + ), + ), + (final Latte previous, final Latte current) => + (previous.order.id == current.order.id) + // A subtle fade when we're just updating an order. + ? Stack( + fit: .passthrough, + children: [ + FadeTransition( + opacity: CurvedAnimation( + parent: animateOut, + curve: Curves.easeOut, + ), + child: widgetForLatte(previous, _previouslyNeededDivider), + ), + FadeTransition( + opacity: animateIn, + child: widgetForLatte(current, widget.addBottomDivider), + ), + ], + ) + // A proper fly in / fly out when changing orders. + : Stack( + fit: .passthrough, + children: [ + FadeTransition( + opacity: CurvedAnimation( + parent: animateOut, + curve: Curves.easeIn, + ), + child: widgetForLatte(previous, _previouslyNeededDivider), + ), + SlideTransition( + position: + Tween( + begin: const Offset(0, 0.05), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animateIn, + curve: Curves.easeOut, + ), + ), + child: FadeTransition( + opacity: animateIn, + child: widgetForLatte(current, widget.addBottomDivider), + ), + ), + ], + ), + }; + } + + @override + void didUpdateWidget(covariant OrderListItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.transitionAnimation != widget.transitionAnimation) { + oldWidget.transitionAnimation.removeStatusListener( + _onAnimationStatusChange, + ); + widget.transitionAnimation.addStatusListener(_onAnimationStatusChange); + } + } + + @override + void dispose() { + widget.transitionAnimation.removeStatusListener(_onAnimationStatusChange); + super.dispose(); + } + + @override + void initState() { + super.initState(); + widget.transitionAnimation.addStatusListener(_onAnimationStatusChange); + } + + void _onAnimationStatusChange(AnimationStatus status) { + if (status == .completed) { + _previous = widget.latte; + _previouslyNeededDivider = widget.addBottomDivider; + } + } +} + +class _OrderWidget extends StatelessWidget { + const _OrderWidget({ + required this.text, + required this.imageUrl, + required this.status, + }); + + final String text; + + final String? imageUrl; + + final OrderStatus status; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + // The available height functions as a "unit of measurement" + // so that the proportions of the design are constant no matter + // how big or small the layout is. + final height = constraints.maxHeight; + + return Row( + children: [ + SizedBox( + width: height * 0.64, + child: imageUrl == null + ? const LatteImageWidget( + imageUrl: OrderList.fallbackImageUrl, + topScaleRatio: 0.50, + thumbnailSize: true, + ) + : LatteImageWidget( + // Adding a key here makes sure the circle avatar + // gets replaced as soon as the image url changes. + // Otherwise, there's a short glitch when new image shows + // up before the transition. + key: Key(imageUrl!), + imageUrl: imageUrl!, + thumbnailSize: true, + ), + ), + SizedBox(width: height * 0.20), + Expanded( + child: WrappedText( + style: (context, theme) => theme.typography.h1.copyWith( + fontSize: height * 0.44, + ), + child: Text( + text, + maxLines: 1, + overflow: .ellipsis, + ), + // TODO(filiph): This should say "Your latte is ready!" + // when recentlyCompleted or above. + ), + ), + if (status == .recentlyCompleted || status == .completed) + const Text('Your latte is ready!').h3 + else + const SizedBox.shrink(), + SizedBox( + width: height * 0.36, + ), + StatusIcon( + icon: Symbols.schedule, + size: height * 0.52, + status: switch (status) { + .visible => .active, + .atBarista => .done, + .recentlyCompleted => .done, + .completed => .done, + }, + ), + SizedBox( + width: height * 0.26, + ), + StatusIcon( + icon: Icons.image, + size: height * 0.52, + status: switch (status) { + .visible => .notYet, + .atBarista => .active, + .recentlyCompleted => .done, + .completed => .done, + }, + ), + SizedBox( + width: height * 0.26, + ), + StatusIcon( + icon: Icons.local_cafe, + size: height * 0.52, + status: switch (status) { + .visible => .notYet, + .atBarista => .notYet, + .recentlyCompleted => .active, + .completed => .done, + }, + ), + SizedBox( + width: height * 0.06, + ), + ], + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/queue/widgets/status_icon.dart b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/status_icon.dart new file mode 100644 index 0000000..40fd547 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/queue/widgets/status_icon.dart @@ -0,0 +1,140 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:genlatte/src/screens/app/app.dart'; + +enum IconStatus { notYet, active, done } + +enum OrderStatus { visible, atBarista, recentlyCompleted, completed } + +class StatusIcon extends StatefulWidget { + const StatusIcon({ + required this.status, + required this.icon, + required this.size, + super.key, + }); + + static const iconFill = 1.0; + + static const iconWeight = 400.0; + + static const iconOpticalSize = 24.0; + + static const iconGrade = 0.0; + + static const notYetColor = AppColors.placeholderGrey; + + static const activeColor = AppColors.googleIntroBlue; + + static const doneColor = AppColors.black; + + final IconStatus status; + final IconData icon; + final double size; + + @override + State createState() => _StatusIconState(); +} + +class _StatusIconState extends State + with SingleTickerProviderStateMixin { + static ui.Image? _textureImage; + + static Future? _loadFuture; + + @override + Widget build(BuildContext context) { + final color = switch (widget.status) { + IconStatus.notYet => StatusIcon.notYetColor, + IconStatus.active => StatusIcon.activeColor, + IconStatus.done => StatusIcon.doneColor, + }; + + final isTexturedActive = + widget.status == IconStatus.active && _textureImage != null; + + final baseIcon = Icon( + widget.icon, + size: widget.size, + fill: StatusIcon.iconFill, + weight: StatusIcon.iconWeight, + grade: StatusIcon.iconGrade, + opticalSize: StatusIcon.iconOpticalSize, + color: isTexturedActive ? const Color(0xFFFFFFFF) : color, + ); + + if (!isTexturedActive) { + return baseIcon; + } + + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (bounds) { + final texture = _textureImage!; + + final imageCenterX = texture.width / 2; + final imageCenterY = texture.height / 2; + final boundsDiagonal = sqrt( + bounds.width * bounds.width + bounds.height * bounds.height, + ); + final scaleFactor = boundsDiagonal / min(texture.width, texture.height); + + final matrix = Matrix4.identity() + ..translateByDouble(bounds.center.dx, bounds.center.dy, 0, 1) + //..rotateZ(-animationValue * 2 * pi) + ..scaleByDouble(scaleFactor, scaleFactor, 1, 1) + ..translateByDouble(-imageCenterX, -imageCenterY, 0, 1); + + return ImageShader( + texture, + TileMode.decal, + TileMode.decal, + matrix.storage, + ); + }, + child: baseIcon, + ); + } + + @override + void didUpdateWidget(StatusIcon oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.status != oldWidget.status) { + if (widget.status == IconStatus.active) { + unawaited(_ensureTextureLoaded()); + } + } + } + + @override + void initState() { + super.initState(); + if (widget.status == IconStatus.active) { + unawaited(_ensureTextureLoaded()); + } + } + + Future _ensureTextureLoaded() async { + if (_textureImage != null) return; + + _loadFuture ??= () async { + final data = await rootBundle.load( + 'assets/io-primary-gradient-mesh-small.jpg', + ); + final codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); + final frame = await codec.getNextFrame(); + _textureImage = frame.image; + }(); + + await _loadFuture; + setState(() {}); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/recent_orders/CONTEXT.md new file mode 100644 index 0000000..04df99c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/CONTEXT.md @@ -0,0 +1,11 @@ +# Kiosk / Barista / Moderator / Queue / Recent Orders Roots + +**Purpose:** +This directory acts as the architectural boundary grouping for the application's distinct personas. + +**Implementation Details:** +- **Barrel Architecture**: This level strictly exports child module directories like `home/` and `widgets/` via an `index` / `barrel` file. +- **Child Contexts**: Please navigate into `home/` or `widgets/` subdirectories to view specific BLoC architectures, View layouts, and complex logic files tailored towards each app persona. + +**Dependencies:** +- Consumed by `core/routing/routes.dart` to map physical URLs to specific screen entry points. diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/CONTEXT.md new file mode 100644 index 0000000..b782c21 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/CONTEXT.md @@ -0,0 +1,26 @@ +# Recent Orders Home + +**Purpose:** +Manages the "Recent Orders" visualization screen, sometimes known as the "Boba Screen" or "Floating Bubble Mode." This is a highly complex, 60fps physics simulation that runs a deterministic 2D elastic collision model to float recently moderated latte images around a display, culminating in a Hero "Thanos Snap" animation when swapping content in and out. + +**Detailed File Overviews:** + +- `recent_orders_home.dart`: + - **Description**: Barrel export file. + +- `recent_orders_home_bloc.dart`: + - **Description**: Simplified state management. + - **Core Logic**: Listens to the `RecentLatteImage` repository and simply pipes a sliced/sorted array of the N newest images into the `RecentOrdersHomeState`. Also tracks the currently `activeImage` selected by a user. + +- `recent_orders_home_view.dart`: + - **Description**: The massively complex physics engine driving the particle visualization system. + - **Core Logic**: + - **Physics Simulation (`_updateLattePositions`)**: Runs an internal `Timer` fired at 16ms (60 fps) calculating distance vectors, horizontal/vertical wall bounces, and inter-bubble elastic collisions (utilizing absolute to relative resolution computations along normal tangents). + - **DOM Management (`_LatteImagesInnerState`)**: Renders bubbles utilizing a "3-Layer rendering strategy". + - Layer 1 renders the stable floating bubbles tracked by the engine matrix. + - Layer 2 tracks "Ghost" bubbles when an old bubble is bumped out of the pool via an incoming array shift. It computes a localized velocity momentum trajectory while applying a dust particle "Thanos Snap" exit. + - Layer 3 renders the detail-Hero view, locking mechanics when a consumer clicks on a bubble to inspect it. + +**Dependencies/Relationships:** +- Deep reliance on mathematical representations tracked in `recent_orders/models` (like `LattePosition` and `Collision`). +- Depends heavily on the animations hosted in `recent_orders/widgets` (like `SquishableLatteImage` and `ThanosSnappable`). diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home.dart new file mode 100644 index 0000000..9f68fe6 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'recent_orders_home_bloc.dart'; +export 'recent_orders_home_view.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home_bloc.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home_bloc.dart new file mode 100644 index 0000000..c19d19c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home_bloc.dart @@ -0,0 +1,105 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +part 'recent_orders_home_bloc.freezed.dart'; + +typedef _Emit = Emitter; + +/// {@template RecentOrdersHomeBloc} +/// {@endtemplate} +class RecentOrdersHomeBloc + extends Bloc { + /// {@macro RecentOrdersHomeBloc} + RecentOrdersHomeBloc({required this.numberOfImages}) + : super(RecentOrdersHomeState.initial()) { + on( + (event, _Emit emit) => switch (event) { + UpdatedRecentImages() => _onUpdatedRecentImages(event, emit), + SetActiveImage() => _onSetActiveImage(event, emit), + }, + ); + + _recentImagesRepository = GetIt.I>(); + + // TODO(craiglabenz): add server timestamps to ensure pulling the N newest + // images + // TODO(craiglabenz): add a limit to only pull N (probably 13 based on + // Figma) most recent images + final stream = _recentImagesRepository.watchList(); + + _recentImagesSubscription = stream.listen((recentImages) { + add(RecentOrdersHomeEvent.updatedRecentImages(recentImages)); + }); + } + + /// The number of recent images to display. + final int numberOfImages; + + late final Repository _recentImagesRepository; + late final StreamSubscription> + _recentImagesSubscription; + + Future _onUpdatedRecentImages( + UpdatedRecentImages event, + _Emit emit, + ) async { + List sortedImages = List.from( + event.images, + )..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + + sortedImages = sortedImages.length > numberOfImages + ? sortedImages.sublist(0, numberOfImages) + : sortedImages; + + emit(state.copyWith(images: sortedImages)); + } + + void _onSetActiveImage(SetActiveImage event, _Emit emit) => + emit(state.copyWith(activeImage: event.activeImage)); + + @override + Future close() { + _recentImagesSubscription.cancel().ignore(); + return super.close(); + } +} + +/// Actions that can be taken on the RecentOrdersHome page. +@Freezed() +sealed class RecentOrdersHomeEvent with _$RecentOrdersHomeEvent { + /// New recent images have arrived from the server. + const factory RecentOrdersHomeEvent.updatedRecentImages( + List images, + ) = UpdatedRecentImages; + + /// The user has toggled the active image. If the user has de-selected the + /// active image, this value will be null. + const factory RecentOrdersHomeEvent.setActiveImage( + RecentLatteImage? activeImage, + ) = SetActiveImage; +} + +/// {@template RecentOrdersHomeState} +/// Complete representation of the RecentOrdersHome page's state. +/// {@endtemplate +@Freezed() +sealed class RecentOrdersHomeState with _$RecentOrdersHomeState { + /// {@macro RecentOrdersHomeState} + const factory RecentOrdersHomeState({ + @Default([]) List images, + RecentLatteImage? activeImage, + }) = _RecentOrdersHomeState; + const RecentOrdersHomeState._(); + + /// Starter state fed to the [RecentOrdersHomeBloc]. + factory RecentOrdersHomeState.initial() => const RecentOrdersHomeState(); +} diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home_bloc.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home_bloc.freezed.dart new file mode 100644 index 0000000..69217e4 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home_bloc.freezed.dart @@ -0,0 +1,612 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'recent_orders_home_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$RecentOrdersHomeEvent { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RecentOrdersHomeEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'RecentOrdersHomeEvent()'; +} + + +} + +/// @nodoc +class $RecentOrdersHomeEventCopyWith<$Res> { +$RecentOrdersHomeEventCopyWith(RecentOrdersHomeEvent _, $Res Function(RecentOrdersHomeEvent) __); +} + + +/// Adds pattern-matching-related methods to [RecentOrdersHomeEvent]. +extension RecentOrdersHomeEventPatterns on RecentOrdersHomeEvent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( UpdatedRecentImages value)? updatedRecentImages,TResult Function( SetActiveImage value)? setActiveImage,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case UpdatedRecentImages() when updatedRecentImages != null: +return updatedRecentImages(_that);case SetActiveImage() when setActiveImage != null: +return setActiveImage(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( UpdatedRecentImages value) updatedRecentImages,required TResult Function( SetActiveImage value) setActiveImage,}){ +final _that = this; +switch (_that) { +case UpdatedRecentImages(): +return updatedRecentImages(_that);case SetActiveImage(): +return setActiveImage(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( UpdatedRecentImages value)? updatedRecentImages,TResult? Function( SetActiveImage value)? setActiveImage,}){ +final _that = this; +switch (_that) { +case UpdatedRecentImages() when updatedRecentImages != null: +return updatedRecentImages(_that);case SetActiveImage() when setActiveImage != null: +return setActiveImage(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( List images)? updatedRecentImages,TResult Function( RecentLatteImage? activeImage)? setActiveImage,required TResult orElse(),}) {final _that = this; +switch (_that) { +case UpdatedRecentImages() when updatedRecentImages != null: +return updatedRecentImages(_that.images);case SetActiveImage() when setActiveImage != null: +return setActiveImage(_that.activeImage);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( List images) updatedRecentImages,required TResult Function( RecentLatteImage? activeImage) setActiveImage,}) {final _that = this; +switch (_that) { +case UpdatedRecentImages(): +return updatedRecentImages(_that.images);case SetActiveImage(): +return setActiveImage(_that.activeImage);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( List images)? updatedRecentImages,TResult? Function( RecentLatteImage? activeImage)? setActiveImage,}) {final _that = this; +switch (_that) { +case UpdatedRecentImages() when updatedRecentImages != null: +return updatedRecentImages(_that.images);case SetActiveImage() when setActiveImage != null: +return setActiveImage(_that.activeImage);case _: + return null; + +} +} + +} + +/// @nodoc + + +class UpdatedRecentImages implements RecentOrdersHomeEvent { + const UpdatedRecentImages(final List images): _images = images; + + + final List _images; + List get images { + if (_images is EqualUnmodifiableListView) return _images; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_images); +} + + +/// Create a copy of RecentOrdersHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$UpdatedRecentImagesCopyWith get copyWith => _$UpdatedRecentImagesCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdatedRecentImages&&const DeepCollectionEquality().equals(other._images, _images)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_images)); + +@override +String toString() { + return 'RecentOrdersHomeEvent.updatedRecentImages(images: $images)'; +} + + +} + +/// @nodoc +abstract mixin class $UpdatedRecentImagesCopyWith<$Res> implements $RecentOrdersHomeEventCopyWith<$Res> { + factory $UpdatedRecentImagesCopyWith(UpdatedRecentImages value, $Res Function(UpdatedRecentImages) _then) = _$UpdatedRecentImagesCopyWithImpl; +@useResult +$Res call({ + List images +}); + + + + +} +/// @nodoc +class _$UpdatedRecentImagesCopyWithImpl<$Res> + implements $UpdatedRecentImagesCopyWith<$Res> { + _$UpdatedRecentImagesCopyWithImpl(this._self, this._then); + + final UpdatedRecentImages _self; + final $Res Function(UpdatedRecentImages) _then; + +/// Create a copy of RecentOrdersHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? images = null,}) { + return _then(UpdatedRecentImages( +null == images ? _self._images : images // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc + + +class SetActiveImage implements RecentOrdersHomeEvent { + const SetActiveImage(this.activeImage); + + + final RecentLatteImage? activeImage; + +/// Create a copy of RecentOrdersHomeEvent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SetActiveImageCopyWith get copyWith => _$SetActiveImageCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SetActiveImage&&(identical(other.activeImage, activeImage) || other.activeImage == activeImage)); +} + + +@override +int get hashCode => Object.hash(runtimeType,activeImage); + +@override +String toString() { + return 'RecentOrdersHomeEvent.setActiveImage(activeImage: $activeImage)'; +} + + +} + +/// @nodoc +abstract mixin class $SetActiveImageCopyWith<$Res> implements $RecentOrdersHomeEventCopyWith<$Res> { + factory $SetActiveImageCopyWith(SetActiveImage value, $Res Function(SetActiveImage) _then) = _$SetActiveImageCopyWithImpl; +@useResult +$Res call({ + RecentLatteImage? activeImage +}); + + +$RecentLatteImageCopyWith<$Res>? get activeImage; + +} +/// @nodoc +class _$SetActiveImageCopyWithImpl<$Res> + implements $SetActiveImageCopyWith<$Res> { + _$SetActiveImageCopyWithImpl(this._self, this._then); + + final SetActiveImage _self; + final $Res Function(SetActiveImage) _then; + +/// Create a copy of RecentOrdersHomeEvent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? activeImage = freezed,}) { + return _then(SetActiveImage( +freezed == activeImage ? _self.activeImage : activeImage // ignore: cast_nullable_to_non_nullable +as RecentLatteImage?, + )); +} + +/// Create a copy of RecentOrdersHomeEvent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$RecentLatteImageCopyWith<$Res>? get activeImage { + if (_self.activeImage == null) { + return null; + } + + return $RecentLatteImageCopyWith<$Res>(_self.activeImage!, (value) { + return _then(_self.copyWith(activeImage: value)); + }); +} +} + +/// @nodoc +mixin _$RecentOrdersHomeState { + + List get images; RecentLatteImage? get activeImage; +/// Create a copy of RecentOrdersHomeState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$RecentOrdersHomeStateCopyWith get copyWith => _$RecentOrdersHomeStateCopyWithImpl(this as RecentOrdersHomeState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RecentOrdersHomeState&&const DeepCollectionEquality().equals(other.images, images)&&(identical(other.activeImage, activeImage) || other.activeImage == activeImage)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(images),activeImage); + +@override +String toString() { + return 'RecentOrdersHomeState(images: $images, activeImage: $activeImage)'; +} + + +} + +/// @nodoc +abstract mixin class $RecentOrdersHomeStateCopyWith<$Res> { + factory $RecentOrdersHomeStateCopyWith(RecentOrdersHomeState value, $Res Function(RecentOrdersHomeState) _then) = _$RecentOrdersHomeStateCopyWithImpl; +@useResult +$Res call({ + List images, RecentLatteImage? activeImage +}); + + +$RecentLatteImageCopyWith<$Res>? get activeImage; + +} +/// @nodoc +class _$RecentOrdersHomeStateCopyWithImpl<$Res> + implements $RecentOrdersHomeStateCopyWith<$Res> { + _$RecentOrdersHomeStateCopyWithImpl(this._self, this._then); + + final RecentOrdersHomeState _self; + final $Res Function(RecentOrdersHomeState) _then; + +/// Create a copy of RecentOrdersHomeState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? images = null,Object? activeImage = freezed,}) { + return _then(_self.copyWith( +images: null == images ? _self.images : images // ignore: cast_nullable_to_non_nullable +as List,activeImage: freezed == activeImage ? _self.activeImage : activeImage // ignore: cast_nullable_to_non_nullable +as RecentLatteImage?, + )); +} +/// Create a copy of RecentOrdersHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$RecentLatteImageCopyWith<$Res>? get activeImage { + if (_self.activeImage == null) { + return null; + } + + return $RecentLatteImageCopyWith<$Res>(_self.activeImage!, (value) { + return _then(_self.copyWith(activeImage: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [RecentOrdersHomeState]. +extension RecentOrdersHomeStatePatterns on RecentOrdersHomeState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _RecentOrdersHomeState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _RecentOrdersHomeState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _RecentOrdersHomeState value) $default,){ +final _that = this; +switch (_that) { +case _RecentOrdersHomeState(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _RecentOrdersHomeState value)? $default,){ +final _that = this; +switch (_that) { +case _RecentOrdersHomeState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List images, RecentLatteImage? activeImage)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _RecentOrdersHomeState() when $default != null: +return $default(_that.images,_that.activeImage);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List images, RecentLatteImage? activeImage) $default,) {final _that = this; +switch (_that) { +case _RecentOrdersHomeState(): +return $default(_that.images,_that.activeImage);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List images, RecentLatteImage? activeImage)? $default,) {final _that = this; +switch (_that) { +case _RecentOrdersHomeState() when $default != null: +return $default(_that.images,_that.activeImage);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _RecentOrdersHomeState extends RecentOrdersHomeState { + const _RecentOrdersHomeState({final List images = const [], this.activeImage}): _images = images,super._(); + + + final List _images; +@override@JsonKey() List get images { + if (_images is EqualUnmodifiableListView) return _images; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_images); +} + +@override final RecentLatteImage? activeImage; + +/// Create a copy of RecentOrdersHomeState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$RecentOrdersHomeStateCopyWith<_RecentOrdersHomeState> get copyWith => __$RecentOrdersHomeStateCopyWithImpl<_RecentOrdersHomeState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecentOrdersHomeState&&const DeepCollectionEquality().equals(other._images, _images)&&(identical(other.activeImage, activeImage) || other.activeImage == activeImage)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_images),activeImage); + +@override +String toString() { + return 'RecentOrdersHomeState(images: $images, activeImage: $activeImage)'; +} + + +} + +/// @nodoc +abstract mixin class _$RecentOrdersHomeStateCopyWith<$Res> implements $RecentOrdersHomeStateCopyWith<$Res> { + factory _$RecentOrdersHomeStateCopyWith(_RecentOrdersHomeState value, $Res Function(_RecentOrdersHomeState) _then) = __$RecentOrdersHomeStateCopyWithImpl; +@override @useResult +$Res call({ + List images, RecentLatteImage? activeImage +}); + + +@override $RecentLatteImageCopyWith<$Res>? get activeImage; + +} +/// @nodoc +class __$RecentOrdersHomeStateCopyWithImpl<$Res> + implements _$RecentOrdersHomeStateCopyWith<$Res> { + __$RecentOrdersHomeStateCopyWithImpl(this._self, this._then); + + final _RecentOrdersHomeState _self; + final $Res Function(_RecentOrdersHomeState) _then; + +/// Create a copy of RecentOrdersHomeState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? images = null,Object? activeImage = freezed,}) { + return _then(_RecentOrdersHomeState( +images: null == images ? _self._images : images // ignore: cast_nullable_to_non_nullable +as List,activeImage: freezed == activeImage ? _self.activeImage : activeImage // ignore: cast_nullable_to_non_nullable +as RecentLatteImage?, + )); +} + +/// Create a copy of RecentOrdersHomeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$RecentLatteImageCopyWith<$Res>? get activeImage { + if (_self.activeImage == null) { + return null; + } + + return $RecentLatteImageCopyWith<$Res>(_self.activeImage!, (value) { + return _then(_self.copyWith(activeImage: value)); + }); +} +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home_view.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home_view.dart new file mode 100644 index 0000000..20f737a --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/home/recent_orders_home_view.dart @@ -0,0 +1,1109 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; +import 'dart:ui' show lerpDouble; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/screens/recent_orders/home/recent_orders_home.dart'; +import 'package:genlatte/src/screens/recent_orders/models/models.dart'; +import 'package:genlatte/src/screens/recent_orders/widgets/widgets.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' hide OverflowMarquee; + +/// {@template RecentOrdersHomeScreen} +/// Initial RecentOrdersHome screen. +/// {@endtemplate} +class RecentOrdersHomeScreen extends StatefulWidget { + /// {@macro RecentOrdersHomeScreen} + const RecentOrdersHomeScreen({super.key}); + + @override + State createState() => _RecentOrdersHomeScreenState(); +} + +class _RecentOrdersHomeScreenState extends State { + final RecentOrdersHomeBloc bloc = RecentOrdersHomeBloc( + numberOfImages: _startingLattePositions.length, + ); + + // TODO(craiglabenz): DELETE THIS. + // This is only for testing the arrival of new images. + // final _recentImagesRepo = RecentLatteImagesRepository(); + + static const _basePortraitSize = Size(720, 1200); + static const _baseLandscapeSize = Size(1200, 720); + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, layoutInfo) { + final scale = layoutInfo.orientation.isLandscape + ? layoutInfo.width / _baseLandscapeSize.width + : layoutInfo.height / _basePortraitSize.height; + + final genLatteBannerTopPadding = 30 * scale; + final genLatteBannerHeight = 50 * scale; + final totalGenLatteHeight = + genLatteBannerTopPadding + genLatteBannerHeight; + return BlocBuilder( + bloc: bloc, + builder: (context, state) { + return SafeArea( + top: !kIsWeb, + left: false, + bottom: false, + right: false, + child: Scaffold( + backgroundColor: AppColors.white, + child: Stack( + children: [ + Positioned.fill( + child: _LatteImagesInner( + activeImage: state.activeImage, + clearActiveImage: () => + bloc.add(const SetActiveImage(null)), + images: state.images, + layoutInfo: layoutInfo, + setActiveImage: (image) => + bloc.add(SetActiveImage(image)), + ), + ), + Positioned( + top: genLatteBannerTopPadding, + width: layoutInfo.width, + height: genLatteBannerHeight, + child: Center( + child: TripleTapDetector( + semanticLabel: 'Header', + semanticHint: 'Triple tap to sign out', + onPressed: () => GetIt.I().signOut(), + child: Text( + 'GenLatte', + style: TextStyle(fontSize: 48 * scale), + ), + ), + ), + ), + Positioned( + top: totalGenLatteHeight + genLatteBannerTopPadding, + height: genLatteBannerHeight, + width: layoutInfo.width, + child: Center( + child: Footer( + fontColor: AppColors.black, + uiScale: layoutInfo.width / 560, + ), + ), + ), + // Positioned( + // right: 20, + // bottom: 20, + // height: 45, + // width: 150, + // child: GenLatteOutlinedButton.dark( + // label: 'Add new image', + // onPressed: _recentImagesRepo.addRandomRecentImage, + // ), + // ), + if (state.activeImage != null) + Positioned.fill( + child: GestureDetector( + onTap: () => bloc.add(const SetActiveImage(null)), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + } + + @override + Future dispose() async { + await bloc.close(); + super.dispose(); + } +} + +/// The inner widget that manages the floating latte images, their physics, +/// and the transition between list and detail views. +class _LatteImagesInner extends StatefulWidget { + const _LatteImagesInner({ + required this.activeImage, + required this.clearActiveImage, + required this.images, + required this.layoutInfo, + required this.setActiveImage, + }); + + final RecentLatteImage? activeImage; + final VoidCallback clearActiveImage; + final List images; + final LayoutInformation layoutInfo; + final void Function(RecentLatteImage) setActiveImage; + + @override + State<_LatteImagesInner> createState() => _LatteImagesInnerState(); +} + +class _LatteImagesInnerState extends State<_LatteImagesInner> + with TickerProviderStateMixin { + /// The current physics state of all latte "slots" on the screen. + List _lattePositions = []; + + /// The timer that drives the 60fps physics simulation. + Timer? _timer; + + /// The images that are currently playing their Thanos snap animation. + final List<_SnappingImage> _snappingImages = []; + + /// The URLs of images that have arrived but are waiting for a Thanos snap + /// to complete before they fade in. + final Set _pendingImageUrls = {}; + + /// A value of 0 means the UI is in the list view, with floating bubbles. + /// Any value between 0 and 1 means the UI is transitioning, and a value of 1 + /// means the UI is in the detail view. + double _selectedLatteImageAnimationProgress = 0; + + /// The image that is currently being showcased in the detail view, or is + /// in the process of animating in or out. + RecentLatteImage? _heroImage; + + /// Controls the transition animation between the grid and detail views. + late AnimationController _selectedLatteImageController; + + /// The number of indices each image has shifted by through the + /// [_startingLattePositions] list. This allows images to rotate through + /// stable positions while looking like they are constantly arriving. + int _imageIndexShift = 0; + + static const double _halfPi = pi / 2; + + @override + void initState() { + super.initState(); + _lattePositions = List.from(_startingLattePositions); + _selectedLatteImageController = + AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + )..addListener(() { + setState(() { + _selectedLatteImageAnimationProgress = + _selectedLatteImageController.value; + + if (_selectedLatteImageAnimationProgress == 0 && + widget.activeImage == null) { + _heroImage = null; + } + }); + }); + + _startTimer(); + } + + void _startTimer() { + _stopTimer(); + + /// 60 fps + _timer = Timer.periodic(const Duration(milliseconds: 16), (timer) { + _updateLattePositions(); + }); + } + + void _stopTimer() => _timer?.cancel(); + + @override + void didUpdateWidget(covariant _LatteImagesInner oldWidget) { + super.didUpdateWidget(oldWidget); + + final didListGrow = widget.images.length != oldWidget.images.length; + final didListShift = + widget.images.firstOrNull?.imageUrl != + oldWidget.images.firstOrNull?.imageUrl; + + if (didListGrow || didListShift) { + // Logic for handling "new image arrival": + // 1. Identify which "slot" in the physics engine will receive the new + // image. + // 2. Take a snapshot of the image currently in that slot. + // 3. Move that image into the "snapping images" list to play its exit + // animation. + // 4. Update the _imageIndexShift so the new image correctly maps to that + // slot. + + final oldShift = _imageIndexShift; + final newShift = (oldShift + 1) % _startingLattePositions.length; + + // The physical slot mapped to index 0 (newest image) after the shift. + final pIndex = + (_startingLattePositions.length - newShift) % + _startingLattePositions.length; + + final newImageUrl = widget.images[0].imageUrl; + final isAddition = widget.images.length > oldWidget.images.length; + + // Find the image index that previously occupied this physical slot. + final oldIndex = (pIndex + oldShift) % _startingLattePositions.length; + + if (!isAddition && + oldIndex < oldWidget.images.length && + oldWidget.images[oldIndex].imageUrl != newImageUrl) { + final removedImage = oldWidget.images[oldIndex]; + final layoutSize = Size( + widget.layoutInfo.width, + widget.layoutInfo.height, + ); + + // Capture the position and velocity of the image exactly at the moment + // it is swapped out to ensure the ghost continues its momentum. + final absPos = _lattePositions[pIndex].toAbsolute(layoutSize); + + final velocity = Offset( + cos(absPos.direction) * absPos.speed, + sin(absPos.direction) * absPos.speed, + ); + + // Track this image as "pending" so it stays invisible until the snap + // finishes. + _pendingImageUrls.add(newImageUrl); + + _snappingImages.add( + _SnappingImage( + image: removedImage, + position: absPos, + associatedNewImageUrl: newImageUrl, + velocity: velocity, + startTime: DateTime.now(), + ), + ); + } + + _calculateImageIndexShift(); + } + + // Handle transitions between the interactive bubble grid and the focused + // detail view. + if (widget.activeImage != oldWidget.activeImage) { + if (widget.activeImage == null) { + _animateToListView(); + } else { + // Prepare image for hero transition. + _heroImage = widget.activeImage; + + // Pause physics for better performance/focus in detail view. + _stopTimer(); + _animateToDetailView(); + } + } + } + + /// The list has changed, which requires us to sort out where the newest + /// image is in the list. + void _calculateImageIndexShift() { + _imageIndexShift++; + if (_imageIndexShift >= _startingLattePositions.length) { + _imageIndexShift = 0; + } + } + + /// The main physics loop: handles movement, repulsion from other lattes, + /// and wall bouncing. + void _updateLattePositions() { + if (widget.layoutInfo.width <= 0 || widget.layoutInfo.height <= 0) return; + + // No need to animate anything while we're in detail mode. + if (_selectedLatteImageAnimationProgress == 1) return; + + final layoutSize = Size(widget.layoutInfo.width, widget.layoutInfo.height); + final updatedPositions = _lattePositions.indexed.map((indexAndPos) { + final i = indexAndPos.$1; + final pos = indexAndPos.$2; + + final index = (i + _imageIndexShift) % _startingLattePositions.length; + if (index >= widget.images.length) { + return pos.toAbsolute(layoutSize); + } + + double targetRadius = pos.radius; + final imageUrl = widget.images[index].imageUrl; + if (_pendingImageUrls.contains(imageUrl)) { + // If the image at this position is still pending its appearance, + // we force it to target its current radius, thus avoiding + // any expansion until it is ready to be shown. + targetRadius = pos.radius; + } else { + targetRadius = _startingLattePositions[index].radius; + } + + final newRadius = pos.radius + (targetRadius - pos.radius) * 0.1; + + return pos.copyWith(radius: newRadius).toAbsolute(layoutSize); + }).toList(); + + // Step 2: Wall Bouncing and Movement + _lattePositions = updatedPositions.indexed.map((indexAndAbs) { + final i = indexAndAbs.$1; + final absPosition = indexAndAbs.$2; + + final index = (i + _imageIndexShift) % _startingLattePositions.length; + if (index >= widget.images.length) { + return absPosition.toRelative(layoutSize); + } + + double direction = absPosition.direction; + double? squishAngle = absPosition.squishAngle; + double speed = absPosition.speed; + + bool horizontalBounce = false; + + // -------- HORIZONTAL BOUNCE (LEFT / RIGHT WALLS) -------- + if ((absPosition.center.dx - absPosition.radius <= 0 && + cos(direction) < 0) || + (absPosition.center.dx + absPosition.radius >= layoutSize.width && + cos(direction) > 0)) { + direction = pi - direction; + horizontalBounce = true; + } + + bool verticalBounce = false; + + // -------- VERTICAL BOUNCE (TOP / BOTTOM WALLS) -------- + if ((absPosition.center.dy - absPosition.radius <= 0 && + sin(direction) < 0) || + (absPosition.center.dy + absPosition.radius >= layoutSize.height && + sin(direction) > 0)) { + direction = -direction; + verticalBounce = true; + } + + // If we bounced against either wall, configure the new squish angle + if (horizontalBounce || verticalBounce) { + squishAngle = horizontalBounce ? pi / 2 : 0; + + // Return speed to absolute representation of default speed. + final vx = + LattePosition.defaultSpeed * cos(direction) * layoutSize.width; + final vy = + LattePosition.defaultSpeed * sin(direction) * layoutSize.height; + speed = sqrt(vx * vx + vy * vy); + } + + final mappedPos = absPosition.copyWith( + center: absPosition.center.copyWith( + dx: absPosition.center.dx + cos(direction) * speed, + dy: absPosition.center.dy + sin(direction) * speed, + ), + direction: direction, + squishAngle: squishAngle, + speed: speed, + lastCollidedWith: horizontalBounce || verticalBounce + ? null + : absPosition.lastCollidedWith, + lastCollisionAt: (horizontalBounce || verticalBounce) + ? DateTime.now() + : null, + ); + + return mappedPos.toRelative(layoutSize); + }).toList(); + + // Step 3: Inter-bubble collisions + // We convert all positions to absolute pixel coordinates once for this pass + final absPositions = _lattePositions + .map((p) => p.toAbsolute(layoutSize)) + .toList(); + + for (int i = 0; i < absPositions.length; i++) { + final indexI = (i + _imageIndexShift) % 13; + if (indexI >= widget.images.length) continue; + + for (int j = i + 1; j < absPositions.length; j++) { + final indexJ = (j + _imageIndexShift) % 13; + if (indexJ >= widget.images.length) continue; + + final a = absPositions[i]; + final b = absPositions[j]; + + // Ensure these two objects didn't just bounce off each other + if (a.lastCollidedWith == b.id && b.lastCollidedWith == a.id) { + continue; + } + + final dx = b.center.dx - a.center.dx; + final dy = b.center.dy - a.center.dy; + + // Use length squared to avoid computing square root prematurely + final lengthSquared = (dx * dx) + (dy * dy); + final minDistance = a.radius + b.radius; + final minDistanceSquared = minDistance * minDistance; + + if (lengthSquared < minDistanceSquared) { + // Calculate the pixel overlap + final distance = sqrt(lengthSquared); + final safeDistance = distance == 0 ? 0.0001 : distance; + final overlap = minDistance - safeDistance; + + final nx = dx / safeDistance; + final ny = dy / safeDistance; + + // Convert pixel separation back to percentages + final moveX = nx * overlap / 2.0; + final moveY = ny * overlap / 2.0; + + // Create translated copies for positional correction + final correctedA = a.copyWith( + center: a.center.translate(-moveX, -moveY), + ); + final correctedB = b.copyWith( + center: b.center.translate(moveX, moveY), + ); + + // Phase 2: Elastic resolution + final vAx = a.speed * cos(a.direction); + final vAy = a.speed * sin(a.direction); + final vBx = b.speed * cos(b.direction); + final vBy = b.speed * sin(b.direction); + + final dotProduct = ((vAx - vBx) * dx) + ((vAy - vBy) * dy); + + if (dotProduct > 0) { + final collision = Collision(correctedA, correctedB); + final result = collision.resolve(); + + // Store the resolved state back into the absolute list + absPositions[i] = correctedA.copyWith( + direction: result.latte1Direction, + squishAngle: result.latte1Squish.direction + _halfPi, + speed: result.latte1Speed, + lastCollisionAt: DateTime.now(), + lastCollidedWith: b.id, + ); + absPositions[j] = correctedB.copyWith( + direction: result.latte2Direction, + squishAngle: result.latte2Squish.direction + _halfPi, + speed: result.latte2Speed, + lastCollisionAt: DateTime.now(), + lastCollidedWith: a.id, + ); + } else { + // Only update positions if we didn't resolve a new velocity + absPositions[i] = correctedA; + absPositions[j] = correctedB; + } + } + } + } + + // Convert back to relative coordinates for the next frame + _lattePositions = absPositions + .map((abs) => abs.toRelative(layoutSize)) + .toList(); + + setState(() {}); + } + + void _toggleActiveImage(RecentLatteImage? image) { + if (widget.activeImage == null) { + assert(image != null, 'Cannot set an active image when none is selected'); + widget.setActiveImage(image!); + } else { + widget.clearActiveImage(); + } + } + + @override + Widget build(BuildContext context) { + if (widget.images.isEmpty) { + return const SizedBox.shrink(); + } + final heroImageUrl = _heroImage?.imageUrl; + AbsoluteLattePosition? heroGridPos; + + final heightScale = widget.layoutInfo.height / 800; + + final showOnListViewOpacity = _selectedLatteImageAnimationProgress; + + // The layout for recent orders uses a 3-layer rendering strategy to handle + // live physics, list changes, and hero transitions simultaneously. + return Stack( + clipBehavior: Clip.none, + children: [ + // Layer 0: Background circles. Decorative elements that fade based on + // whether the detail view (activeImage) is visible. + ..._filledCircleData.map( + (circle) => circle.toWidget( + widget.layoutInfo, + isDetail: widget.activeImage != null, + ), + ), + + // Layer 1: Stable floating bubbles. + // These represent the images in the actual grid as managed by the + // physics engine. We map physical slots (0-12) to these bubbles. + ..._lattePositions.indexed.expand( + (indexAndPosition) sync* { + final position = indexAndPosition.$2; + final slotIndex = indexAndPosition.$1; + + final index = + (slotIndex + _imageIndexShift) % _startingLattePositions.length; + + if (index >= widget.images.length) { + yield const SizedBox.shrink(); + return; + } + + final image = widget.images[index]; + final isHero = image.imageUrl == heroImageUrl; + + final absPos = position.toAbsolute( + Size(widget.layoutInfo.width, widget.layoutInfo.height), + ); + + if (isHero) { + heroGridPos = absPos; + } + + // To keep coordinate systems consistent between the grid and the + // hero transition, we use a "3R" rule: each image container is + // exactly 3x the radius of the image, centered on the image's + // coordinate. + final boxSize = absPos.radius * 3; + + final isPending = _pendingImageUrls.contains(image.imageUrl); + + final hideOnHeroOpacity = isHero + ? 0.0 + : (1.0 - _selectedLatteImageAnimationProgress); + yield Positioned( + left: absPos.center.dx - boxSize / 2, + top: absPos.center.dy - boxSize / 2, + height: boxSize, + width: boxSize, + key: ValueKey('position-${position.id}'), + child: Opacity( + // Fade out non-hero images during detail transition. + opacity: hideOnHeroOpacity, + child: AnimatedOpacity( + // Fade in new images when they arrive. + opacity: isPending ? 0.0 : 1.0, + duration: isPending + ? Duration.zero + : const Duration(milliseconds: 500), + child: IgnorePointer( + ignoring: widget.activeImage != null, + child: SquishableLatteImage( + image: image, + lattePosition: isHero + ? absPos.copyWith(squishAngle: null) + : absPos, + ), + ), + ), + ), + ); + + if (widget.activeImage == null) { + // Now add a hit box for the latte image, which is different from + // its own rendering bounding box, which has to have extra padding + // to accommodate the wobble animation. However, that extra + // padding can cause close-proximity images to overlap one another + yield Positioned( + left: absPos.center.dx - boxSize / 2 + absPos.radius / 2, + top: absPos.center.dy - boxSize / 2 + absPos.radius / 2, + + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => _toggleActiveImage(image), + child: SizedBox.fromSize( + size: Size.square(absPos.radius * 2), + ), + ), + ); + } + }, + ), + + // Layer 2: "Ghosts" of lattes that have just been replaced. + // When a new image arrives, it displaces an old one. This layer + // captures the displaced image and applies a "Thanos snap" effect while + // preserving its momentum from the physics loop. + ..._snappingImages.map((snapping) { + final padding = snapping.position.radius * 2; + final elapsedTime = DateTime.now().difference(snapping.startTime); + final displacement = + snapping.velocity * (elapsedTime.inMicroseconds / 16666.0); + final center = snapping.position.center + displacement; + + return Positioned( + key: ValueKey('snap-${snapping.associatedNewImageUrl}'), + left: center.dx - snapping.position.radius - padding, + top: center.dy - snapping.position.radius - padding, + height: snapping.position.radius * 2 + padding * 2, + width: snapping.position.radius * 2 + padding * 2, + child: Center( + child: ThanosSnappable( + onComplete: () { + if (mounted) { + setState(() { + _snappingImages.remove(snapping); + _pendingImageUrls.remove( + snapping.associatedNewImageUrl, + ); + }); + } + }, + child: SquishableLatteImage( + image: snapping.image, + lattePosition: snapping.position.copyWith( + center: center, + ), + ), + ), + ), + ); + }), + + // Layer 3: The hero image animating to the front and center. + // This is a single image that "breaks out" of Layer 1 to become the + // central focus of the detail view. It tracks its original grid + // position (captured in Layer 1 as 'heroGridPos') to ensure seamless + // takeoff and landing. + if (_heroImage != null && heroGridPos != null) ...[ + Positioned( + left: widget.layoutInfo.width * 0.1, + width: widget.layoutInfo.width * 0.8, + top: + widget.layoutInfo.height * 0.17 + + // Add some more top padding for square aspect ratios because + // that keeps the footer from crowding the image explainer text + // too much. + (widget.layoutInfo.aspectRatio > 1.2 && + widget.layoutInfo.aspectRatio < 1.4 + ? widget.layoutInfo.height * 0.03 + : 0), + bottom: 0, + child: Opacity( + opacity: showOnListViewOpacity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: widget.layoutInfo.aspectRatio < 1.4 + ? MainAxisAlignment.start + : MainAxisAlignment.center, + children: [ + Text('${_heroImage!.name}:').h1, + const Text('My happy place is').h2_, + SizedBox(height: 16 * heightScale), + Text('“${_heroImage!.happyPlace}”').h3, + SizedBox(height: 8 * heightScale), + SizedBox( + width: + widget.layoutInfo.width * + (widget.layoutInfo.aspectRatio < 1.4 ? 0.6 : 0.4), + child: Row( + children: [ + const Text( + 'Prompt: ', + style: TextStyle(fontWeight: .bold), + ), + SizedBox(width: 8 * heightScale), + Flexible( + flex: 3, + child: OverflowMarquee(text: _heroImage!.prompt), + ), + ], + ), + ), + ], + ), + ), + ), + _HeroLatte( + image: _heroImage!, + gridPosition: heroGridPos!, + layoutInfo: widget.layoutInfo, + progress: _selectedLatteImageAnimationProgress, + onTap: widget.clearActiveImage, + ), + ], + ], + ); + } + + void _animateToListView() { + _selectedLatteImageController + .animateTo(0, curve: Curves.easeInOutExpo) + .then((_) { + if (mounted && widget.activeImage == null) { + _startTimer(); + } + }) + .ignore(); + } + + void _animateToDetailView() { + _selectedLatteImageController + .animateTo(1, curve: Curves.easeInOutExpo) + .ignore(); + } + + @override + Future dispose() async { + _timer?.cancel(); + super.dispose(); + } +} + +class _FilledCircleData { + const _FilledCircleData({ + required this.color, + required this.radius, + required this.position, + this.showOnDetailView = true, + }); + + final Color color; + final double radius; + final Offset position; + + /// Set to false if this appears behind or even too close to lettering + final bool showOnDetailView; + + /// Arbitrary constant used to scale the background circles to the screen + /// size. + static const double _baseSize = 500; + + Widget toWidget(LayoutInformation layoutInfo, {required bool isDetail}) { + final scale = layoutInfo.constrainingDimension / _baseSize; + return _FilledCircle( + color: color, + radius: radius * scale, + position: Offset( + position.dx * layoutInfo.width, + position.dy * layoutInfo.height, + ), + opacity: showOnDetailView || !isDetail ? 1.0 : 0.0, + ); + } +} + +class _FilledCircle extends StatelessWidget { + const _FilledCircle({ + required this.color, + required this.radius, + required this.position, + required this.opacity, + }); + + final Color color; + final double radius; + final Offset position; + final double opacity; + + @override + Widget build(BuildContext context) { + return Positioned( + left: position.dx, + top: position.dy, + width: radius * 2, + height: radius * 2, + key: ValueKey('$position-$radius-$color'), + child: AnimatedOpacity( + opacity: opacity, + duration: const Duration(milliseconds: 600), + child: CustomPaint( + painter: _FilledCirclePainter(color: color), + ), + ), + ); + } +} + +class _FilledCirclePainter extends CustomPainter { + _FilledCirclePainter({required this.color}); + + final Color color; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + canvas.drawCircle( + Offset(size.width / 2, size.height / 2), + size.width / 2, + paint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +const _filledCircleData = [ + _FilledCircleData( + color: AppColors.googleIntroRed, + radius: 10, + position: Offset(0.1, 0.1), + ), + _FilledCircleData( + color: AppColors.googleIntroGreen, + radius: 5, + position: Offset(0.9, 0.1), + ), + _FilledCircleData( + color: AppColors.googleIntroBlue, + radius: 8, + position: Offset(0.4, 0.7), + ), + _FilledCircleData( + color: AppColors.googleIntroBlue, + radius: 12, + position: Offset(0.7, 0.35), + ), + _FilledCircleData( + color: AppColors.googleIntroRed, + radius: 15, + position: Offset(0.2, 0.65), + showOnDetailView: false, + ), + _FilledCircleData( + color: AppColors.latteArtGold, + radius: 12, + position: Offset(0.8, 0.75), + ), + _FilledCircleData( + color: AppColors.latteArtGold, + radius: 6, + position: Offset(0.2, 0.21), + showOnDetailView: false, + ), + _FilledCircleData( + color: AppColors.googleIntroRed, + radius: 9, + position: Offset(0.75, 0.22), + ), + _FilledCircleData( + color: AppColors.latteArtGold, + radius: 11, + position: Offset(0.5, 0.5), + ), + _FilledCircleData( + color: AppColors.googleIntroBlue, + radius: 7, + position: Offset(0.3, 0.45), + showOnDetailView: false, + ), + _FilledCircleData( + color: AppColors.googleIntroRed, + radius: 13, + position: Offset(0.7, 0.8), + ), + _FilledCircleData( + color: AppColors.latteArtGold, + radius: 13, + position: Offset(0.15, 0.8), + ), + _FilledCircleData( + color: AppColors.googleIntroGreen, + radius: 8, + position: Offset(0.2, 0.3), + showOnDetailView: false, + ), + _FilledCircleData( + color: AppColors.googleIntroGreen, + radius: 8, + position: Offset(0.45, 0.85), + ), +]; + +/// The predefined "home" positions for floating latte images. +/// These use relative percentages (0.0 to 1.0) to ensure the design remains +/// responsive regardless of screen size. +const _startingLattePositions = [ + LattePosition( + id: '1', + center: Offset(0.5, 0.5), + radius: 0.120, + direction: 0.5, + squishAngle: null, + ), + LattePosition( + id: '2', + center: Offset(0.2, 0.3), + radius: 0.110, + direction: 1.2, + squishAngle: null, + ), + LattePosition( + id: '3', + center: Offset(0.8, 0.1), + radius: 0.102, + direction: 2.1, + squishAngle: null, + ), + LattePosition( + id: '4', + center: Offset(0.7, 0.7), + radius: 0.093, + direction: 3.5, + squishAngle: null, + ), + LattePosition( + id: '5', + center: Offset(0.3, 0.8), + radius: 0.086, + direction: 4.8, + squishAngle: null, + ), + LattePosition( + id: '6', + center: Offset(0.1, 0.6), + radius: 0.079, + direction: 5.9, + squishAngle: null, + ), + LattePosition( + id: '7', + center: Offset(0.9, 0.5), + radius: 0.073, + direction: 0.2, + squishAngle: null, + ), + LattePosition( + id: '8', + center: Offset(0.4, 0.2), + radius: 0.067, + direction: 1.7, + squishAngle: null, + ), + LattePosition( + id: '9', + center: Offset(0.6, 0.9), + radius: 0.062, + direction: 2.9, + squishAngle: null, + ), + LattePosition( + id: '10', + center: Offset(0.8, 0.8), + radius: 0.057, + direction: 4.1, + squishAngle: null, + ), + LattePosition( + id: '11', + center: Offset(0.2, 0.9), + radius: 0.052, + direction: 5.3, + squishAngle: null, + ), + LattePosition( + id: '12', + center: Offset(0.9, 0.9), + radius: 0.048, + direction: 6.1, + squishAngle: null, + ), + LattePosition( + id: '13', + center: Offset(0.4, 0.5), + radius: 0.044, + direction: 3.14, + squishAngle: null, + ), +]; + +/// Internal record used to manage the state of an image currently undergoing +/// the "Thanos snap" disintegration animation. +class _SnappingImage { + _SnappingImage({ + required this.image, + required this.position, + required this.associatedNewImageUrl, + required this.velocity, + required this.startTime, + }); + + final RecentLatteImage image; + final AbsoluteLattePosition position; + final String associatedNewImageUrl; + final Offset velocity; + final DateTime startTime; +} + +class _HeroLatte extends StatelessWidget { + const _HeroLatte({ + required this.image, + required this.gridPosition, + required this.layoutInfo, + required this.progress, + required this.onTap, + }); + + final RecentLatteImage image; + final AbsoluteLattePosition gridPosition; + final LayoutInformation layoutInfo; + final double progress; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + // The target center for the detail view. + // We adjust the focus point slightly based on aspect ratio to avoid + // crowding the footer text in landscape or tablet views. + final center = layoutInfo.aspectRatio < 1.4 + ? Offset(layoutInfo.width / 2, layoutInfo.height * 0.67) + : Offset(layoutInfo.width * 0.75, layoutInfo.height * 0.6); + + // The target size for the detail view. + // We want the image to be substantial but not overwhelming. + final targetRadius = layoutInfo.aspectRatio < 1.4 + ? layoutInfo.height * 0.14 + : layoutInfo.width * 0.10; + + // Coordinate Interpolation (The "Hero" part): + // We calculate a current position and radius that is a mix of the image's + // 'home' position in the grid and its 'focus' position in the detail view. + final currentCenter = Offset.lerp(gridPosition.center, center, progress)!; + final currentRadius = lerpDouble( + gridPosition.radius, + targetRadius, + progress, + )!; + + // Transitioning Layout Math: + // To prevent the image from "snapping" or "jumping" when it takes off + // from the grid, it's vital that the Positioned box math matches exactly + // across all layers. We use the '3R' rule established in Layer 1. + final boxSize = currentRadius * 3; + + return Positioned( + top: currentCenter.dy - boxSize / 2, + left: currentCenter.dx - boxSize / 2, + width: boxSize, + height: boxSize, + child: SquishableLatteImage( + image: image, + lattePosition: gridPosition.copyWith( + center: currentCenter, + radius: currentRadius, + ), + paddingPercentage: 1 - progress, + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/CONTEXT.md new file mode 100644 index 0000000..a3c8f0b --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/CONTEXT.md @@ -0,0 +1,22 @@ +# Recent Orders Models + +**Purpose:** +Hosts the mathematical data constructs necessary to run the 60fps physics simulation for the "Boba Mode" Recent Orders bubble visualization. These exist outside the presentation layer to separate pure physics calculations from DOM rendering. + +**Detailed File Overviews:** + +- `models.dart`: + - **Description**: Barrel export file. + +- `latte_position.dart`: + - **Description**: Core physics matrices and mathematical formulas. + - **Core Logic**: + - Exposes dual mathematical representations of bounding boxes (`LattePosition` using relative coordinate percentages `[0.0-1.0]` vs `AbsoluteLattePosition` using static `` constraints) which resolves display responsiveness issues during resizing events. + - Encapsulates the core `Collision.resolve()` math that uses elastic vector dot-products over tangent impact lines to compute vector momentum conservation after two spheres collide. + +- `wobble_calculator.dart`: + - **Description**: A mathematical function generator. + - **Core Logic**: Drives the logic behind `SquishableLatteImage` that reduces sine-wave amplitude over time following a collision impact to simulate elastic "jelly" properties. + +**Dependencies/Relationships:** +- Serves as the math/state backing for components in both `recent_orders/home` and `recent_orders/widgets`. diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/latte_position.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/latte_position.dart new file mode 100644 index 0000000..cd2ed2f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/latte_position.dart @@ -0,0 +1,324 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/animation.dart' show Curves; +import 'package:flutter/widgets.dart' show Offset, Size; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'latte_position.freezed.dart'; + +/// A latte position represented entirely in absolute pixel coordinates. +@freezed +abstract class AbsoluteLattePosition with _$AbsoluteLattePosition { + /// Creates a new [AbsoluteLattePosition]. + const factory AbsoluteLattePosition({ + /// Unique identifier to track collision pairs. + required String id, + + /// The center of the latte image. + required Offset center, + + /// The angle of this Latte's movement in radians. + required double direction, + + /// The radius of the latte image as a percentage of the most constraine + /// dimension of the available space. A value of 1.0 would indicate a latte + /// that fills the entire available space. + required double radius, + + /// The angle of the squish effect. + required double? squishAngle, + + /// The speed of this latte's movement. + required double speed, + + /// The last time this latte was involved in a collision. + DateTime? lastCollisionAt, + + /// The ID of the last object this collided with. + String? lastCollidedWith, + }) = _AbsoluteLattePosition; + + const AbsoluteLattePosition._(); + + /// The mass of this latte, used for physics calculations. + double get mass => radius * radius; + + /// Flattens an [AbsoluteLattePosition] into a relative values format, using + /// the current screen dimensions for this frame. + LattePosition toRelative(Size layoutSize) { + final relCx = center.dx / layoutSize.width; + final relCy = center.dy / layoutSize.height; + + final vx = speed * cos(direction); + final vy = speed * sin(direction); + + final relVx = vx / layoutSize.width; + final relVy = vy / layoutSize.height; + final relSpeed = sqrt(relVx * relVx + relVy * relVy); + final relDir = atan2(relVy, relVx); + + return LattePosition( + id: id, + center: Offset(relCx, relCy), + radius: radius / layoutSize.shortestSide, + direction: relDir, + rawSpeed: relSpeed, + squishAngle: squishAngle, + lastCollisionAt: lastCollisionAt, + lastCollidedWith: lastCollidedWith, + ); + } +} + +/// Represents the position of a latte image on the screen. +@freezed +abstract class LattePosition with _$LattePosition { + /// Creates a new [LattePosition]. + const factory LattePosition({ + /// Unique identifier to track collision pairs. + required String id, + + /// The center of the latte image. + required Offset center, + + /// The angle of this Latte's movement in radians. + required double direction, + + /// Size of the latte image as a percentage of the most constrained + /// dimension of the available space. A value of 1.0 would indicate a latte. + required double radius, + + /// The angle of the squish effect. + required double? squishAngle, + + /// The raw speed as calculated off of the last collision. If this is + /// greater than [defaultSpeed], then friction will slow the latte down + /// until it returns to [defaultSpeed] + @Default(LattePosition.defaultSpeed) double rawSpeed, + + /// The last time this latte was involved in a collision. This is used to + /// calculate friction to slow down a circle's velocity. + DateTime? lastCollisionAt, + + /// The ID of the last object this collided with to prevent rapid duplicate + /// collisions. + String? lastCollidedWith, + }) = _LattePosition; + + const LattePosition._(); + + // : rawSpeed = speed, + // mass = ; + + /// The speed at which each ball wants to move. + static const defaultSpeed = 0.0002; + + /// Mass of the latte image, used for physics calculations. + double get mass => radius * radius; + + static const Duration _decelerationDuration = Duration(seconds: 3); + + /// The speed of this Latte's movement. + double get speed { + if (rawSpeed <= defaultSpeed) { + return rawSpeed; + } + + final timeSinceLastCollision = lastCollisionAt == null + ? Duration.zero + : DateTime.now().difference(lastCollisionAt!); + + if (lastCollisionAt == null || + timeSinceLastCollision >= _decelerationDuration) { + return rawSpeed; + } + return defaultSpeed + + ((rawSpeed - defaultSpeed) * + Curves.easeOut.transform( + timeSinceLastCollision.inMicroseconds / + _decelerationDuration.inMicroseconds, + )); + } + + /// Converts a [LattePosition] into a format of absolute pixel values, using + /// the current screen dimensions for this frame. + AbsoluteLattePosition toAbsolute(Size layoutSize) { + final absRadius = radius * layoutSize.shortestSide; + + final vx = speed * cos(direction) * layoutSize.width; + final vy = speed * sin(direction) * layoutSize.height; + + final absSpeed = sqrt(vx * vx + vy * vy); + final absDir = atan2(vy, vx); + + return AbsoluteLattePosition( + id: id, + center: Offset( + center.dx * layoutSize.width, + center.dy * layoutSize.height, + ), + direction: absDir, + radius: absRadius, + squishAngle: squishAngle, + speed: absSpeed, + lastCollisionAt: lastCollisionAt, + lastCollidedWith: lastCollidedWith, + ); + } +} + +/// Two circles that have collided. +class Collision { + /// Creates a new [Collision]. + Collision(this.c1, this.c2); + + /// The first circle. + final AbsoluteLattePosition c1; + + /// The second circle. + final AbsoluteLattePosition c2; + + /// Resolve this collision. + CollisionResult resolve() { + // 1. Calculate the normal vector (line connecting centers in pixel space) + final dx = c2.center.dx - c1.center.dx; + final dy = c2.center.dy - c1.center.dy; + + // Calculate the angles of the normal line + final c1SquishAngle = atan2(dy, dx); + final c2SquishAngle = atan2(-dy, -dx); // Exact opposite direction + + final distance = sqrt(dx * dx + dy * dy); + + // Prevent division by zero if centers perfectly overlap + if (distance == 0) { + return CollisionResult( + c1.speed, + c1.direction, + c2.speed, + c2.direction, + SquishData(c1SquishAngle, 0), + SquishData(c2SquishAngle, 0), + ); + } + + // Unit normal vector + final nx = dx / distance; + final ny = dy / distance; + + // Unit tangent vector (perpendicular to normal) + final tx = -ny; + final ty = nx; + + // 2. Convert speed and direction to purely Pixel velocity vectors + final c1Vx = c1.speed * cos(c1.direction); + final c1Vy = c1.speed * sin(c1.direction); + final c2Vx = c2.speed * cos(c2.direction); + final c2Vy = c2.speed * sin(c2.direction); + + // 3. Project velocities onto normal and tangent vectors (Dot Product) + final c1Vn = c1Vx * nx + c1Vy * ny; + final c1Vt = c1Vx * tx + c1Vy * ty; + final c2Vn = c2Vx * nx + c2Vy * ny; + final c2Vt = c2Vx * tx + c2Vy * ty; + + final relativeNormalVelocity = c1Vn - c2Vn; + final impactForce = relativeNormalVelocity.abs(); + + final c1Squish = SquishData(c1SquishAngle, impactForce); + final c2Squish = SquishData(c2SquishAngle, impactForce); + + // 4. Resolve the 1D collision along the normal vector + final m1 = c1.mass; + final m2 = c2.mass; + + final c1VnAfter = (c1Vn * (m1 - m2) + 2 * m2 * c2Vn) / (m1 + m2); + final c2VnAfter = (c2Vn * (m2 - m1) + 2 * m1 * c1Vn) / (m1 + m2); + + // Tangential velocities do not change (c1_vt_after = c1_vt) + + // 5. Convert the scalar normal and tangential velocities back into Pixel + // vectors. + // Vector = (Normal_Scalar * Normal_Vector) + (Tangent_Scalar * + // Tangent_Vector) + final c1VxAfter = (c1VnAfter * nx) + (c1Vt * tx); + final c1VyAfter = (c1VnAfter * ny) + (c1Vt * ty); + + final c2VxAfter = (c2VnAfter * nx) + (c2Vt * tx); + final c2VyAfter = (c2VnAfter * ny) + (c2Vt * ty); + + // 6. Convert Pixel X/Y back to Speed and Direction (Polar) + final c1SpeedAfter = sqrt(c1VxAfter * c1VxAfter + c1VyAfter * c1VyAfter); + final c1DirAfter = atan2(c1VyAfter, c1VxAfter); + + final c2SpeedAfter = sqrt(c2VxAfter * c2VxAfter + c2VyAfter * c2VyAfter); + final c2DirAfter = atan2(c2VyAfter, c2VxAfter); + + return CollisionResult( + c1SpeedAfter, + c1DirAfter, + c2SpeedAfter, + c2DirAfter, + c1Squish, + c2Squish, + ); + } +} + +/// The result of a collision. +class CollisionResult { + /// Creates a new [CollisionResult]. + CollisionResult( + this.latte1Speed, + this.latte1Direction, + this.latte2Speed, + this.latte2Direction, + this.latte1Squish, + this.latte2Squish, + ); + + /// The speed of the first circle. + final double latte1Speed; + + /// The direction of the first circle in radians. + final double latte1Direction; + + /// The speed of the second circle. + final double latte2Speed; + + /// The direction of the second circle in radians. + final double latte2Direction; + + /// The squish data for the first circle. + final SquishData latte1Squish; + + /// The squish data for the second circle. + final SquishData latte2Squish; +} + +/// Holds the data for a squish effect. +class SquishData { + /// Creates a new [SquishData]. + SquishData(this.direction, this.intensity); + + /// The direction of the squish effect in radians. + final double direction; + + /// The intensity of the squish effect, representing the force of impact. + final double intensity; +} + +/// Extension to add copyWith to Offset. +extension CopyableOffset on Offset { + /// Creates a copy of this [Offset]. + Offset copyWith({ + double? dx, + double? dy, + }) { + return Offset(dx ?? this.dx, dy ?? this.dy); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/latte_position.freezed.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/latte_position.freezed.dart new file mode 100644 index 0000000..b6980ee --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/latte_position.freezed.dart @@ -0,0 +1,620 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'latte_position.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$AbsoluteLattePosition { + +/// Unique identifier to track collision pairs. + String get id;/// The center of the latte image. + Offset get center;/// The angle of this Latte's movement in radians. + double get direction;/// The radius of the latte image as a percentage of the most constraine +/// dimension of the available space. A value of 1.0 would indicate a latte +/// that fills the entire available space. + double get radius;/// The angle of the squish effect. + double? get squishAngle;/// The speed of this latte's movement. + double get speed;/// The last time this latte was involved in a collision. + DateTime? get lastCollisionAt;/// The ID of the last object this collided with. + String? get lastCollidedWith; +/// Create a copy of AbsoluteLattePosition +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AbsoluteLattePositionCopyWith get copyWith => _$AbsoluteLattePositionCopyWithImpl(this as AbsoluteLattePosition, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AbsoluteLattePosition&&(identical(other.id, id) || other.id == id)&&(identical(other.center, center) || other.center == center)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.radius, radius) || other.radius == radius)&&(identical(other.squishAngle, squishAngle) || other.squishAngle == squishAngle)&&(identical(other.speed, speed) || other.speed == speed)&&(identical(other.lastCollisionAt, lastCollisionAt) || other.lastCollisionAt == lastCollisionAt)&&(identical(other.lastCollidedWith, lastCollidedWith) || other.lastCollidedWith == lastCollidedWith)); +} + + +@override +int get hashCode => Object.hash(runtimeType,id,center,direction,radius,squishAngle,speed,lastCollisionAt,lastCollidedWith); + +@override +String toString() { + return 'AbsoluteLattePosition(id: $id, center: $center, direction: $direction, radius: $radius, squishAngle: $squishAngle, speed: $speed, lastCollisionAt: $lastCollisionAt, lastCollidedWith: $lastCollidedWith)'; +} + + +} + +/// @nodoc +abstract mixin class $AbsoluteLattePositionCopyWith<$Res> { + factory $AbsoluteLattePositionCopyWith(AbsoluteLattePosition value, $Res Function(AbsoluteLattePosition) _then) = _$AbsoluteLattePositionCopyWithImpl; +@useResult +$Res call({ + String id, Offset center, double direction, double radius, double? squishAngle, double speed, DateTime? lastCollisionAt, String? lastCollidedWith +}); + + + + +} +/// @nodoc +class _$AbsoluteLattePositionCopyWithImpl<$Res> + implements $AbsoluteLattePositionCopyWith<$Res> { + _$AbsoluteLattePositionCopyWithImpl(this._self, this._then); + + final AbsoluteLattePosition _self; + final $Res Function(AbsoluteLattePosition) _then; + +/// Create a copy of AbsoluteLattePosition +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? center = null,Object? direction = null,Object? radius = null,Object? squishAngle = freezed,Object? speed = null,Object? lastCollisionAt = freezed,Object? lastCollidedWith = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,center: null == center ? _self.center : center // ignore: cast_nullable_to_non_nullable +as Offset,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as double,radius: null == radius ? _self.radius : radius // ignore: cast_nullable_to_non_nullable +as double,squishAngle: freezed == squishAngle ? _self.squishAngle : squishAngle // ignore: cast_nullable_to_non_nullable +as double?,speed: null == speed ? _self.speed : speed // ignore: cast_nullable_to_non_nullable +as double,lastCollisionAt: freezed == lastCollisionAt ? _self.lastCollisionAt : lastCollisionAt // ignore: cast_nullable_to_non_nullable +as DateTime?,lastCollidedWith: freezed == lastCollidedWith ? _self.lastCollidedWith : lastCollidedWith // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [AbsoluteLattePosition]. +extension AbsoluteLattePositionPatterns on AbsoluteLattePosition { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AbsoluteLattePosition value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AbsoluteLattePosition() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AbsoluteLattePosition value) $default,){ +final _that = this; +switch (_that) { +case _AbsoluteLattePosition(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AbsoluteLattePosition value)? $default,){ +final _that = this; +switch (_that) { +case _AbsoluteLattePosition() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, Offset center, double direction, double radius, double? squishAngle, double speed, DateTime? lastCollisionAt, String? lastCollidedWith)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AbsoluteLattePosition() when $default != null: +return $default(_that.id,_that.center,_that.direction,_that.radius,_that.squishAngle,_that.speed,_that.lastCollisionAt,_that.lastCollidedWith);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, Offset center, double direction, double radius, double? squishAngle, double speed, DateTime? lastCollisionAt, String? lastCollidedWith) $default,) {final _that = this; +switch (_that) { +case _AbsoluteLattePosition(): +return $default(_that.id,_that.center,_that.direction,_that.radius,_that.squishAngle,_that.speed,_that.lastCollisionAt,_that.lastCollidedWith);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, Offset center, double direction, double radius, double? squishAngle, double speed, DateTime? lastCollisionAt, String? lastCollidedWith)? $default,) {final _that = this; +switch (_that) { +case _AbsoluteLattePosition() when $default != null: +return $default(_that.id,_that.center,_that.direction,_that.radius,_that.squishAngle,_that.speed,_that.lastCollisionAt,_that.lastCollidedWith);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _AbsoluteLattePosition extends AbsoluteLattePosition { + const _AbsoluteLattePosition({required this.id, required this.center, required this.direction, required this.radius, required this.squishAngle, required this.speed, this.lastCollisionAt, this.lastCollidedWith}): super._(); + + +/// Unique identifier to track collision pairs. +@override final String id; +/// The center of the latte image. +@override final Offset center; +/// The angle of this Latte's movement in radians. +@override final double direction; +/// The radius of the latte image as a percentage of the most constraine +/// dimension of the available space. A value of 1.0 would indicate a latte +/// that fills the entire available space. +@override final double radius; +/// The angle of the squish effect. +@override final double? squishAngle; +/// The speed of this latte's movement. +@override final double speed; +/// The last time this latte was involved in a collision. +@override final DateTime? lastCollisionAt; +/// The ID of the last object this collided with. +@override final String? lastCollidedWith; + +/// Create a copy of AbsoluteLattePosition +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AbsoluteLattePositionCopyWith<_AbsoluteLattePosition> get copyWith => __$AbsoluteLattePositionCopyWithImpl<_AbsoluteLattePosition>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AbsoluteLattePosition&&(identical(other.id, id) || other.id == id)&&(identical(other.center, center) || other.center == center)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.radius, radius) || other.radius == radius)&&(identical(other.squishAngle, squishAngle) || other.squishAngle == squishAngle)&&(identical(other.speed, speed) || other.speed == speed)&&(identical(other.lastCollisionAt, lastCollisionAt) || other.lastCollisionAt == lastCollisionAt)&&(identical(other.lastCollidedWith, lastCollidedWith) || other.lastCollidedWith == lastCollidedWith)); +} + + +@override +int get hashCode => Object.hash(runtimeType,id,center,direction,radius,squishAngle,speed,lastCollisionAt,lastCollidedWith); + +@override +String toString() { + return 'AbsoluteLattePosition(id: $id, center: $center, direction: $direction, radius: $radius, squishAngle: $squishAngle, speed: $speed, lastCollisionAt: $lastCollisionAt, lastCollidedWith: $lastCollidedWith)'; +} + + +} + +/// @nodoc +abstract mixin class _$AbsoluteLattePositionCopyWith<$Res> implements $AbsoluteLattePositionCopyWith<$Res> { + factory _$AbsoluteLattePositionCopyWith(_AbsoluteLattePosition value, $Res Function(_AbsoluteLattePosition) _then) = __$AbsoluteLattePositionCopyWithImpl; +@override @useResult +$Res call({ + String id, Offset center, double direction, double radius, double? squishAngle, double speed, DateTime? lastCollisionAt, String? lastCollidedWith +}); + + + + +} +/// @nodoc +class __$AbsoluteLattePositionCopyWithImpl<$Res> + implements _$AbsoluteLattePositionCopyWith<$Res> { + __$AbsoluteLattePositionCopyWithImpl(this._self, this._then); + + final _AbsoluteLattePosition _self; + final $Res Function(_AbsoluteLattePosition) _then; + +/// Create a copy of AbsoluteLattePosition +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? center = null,Object? direction = null,Object? radius = null,Object? squishAngle = freezed,Object? speed = null,Object? lastCollisionAt = freezed,Object? lastCollidedWith = freezed,}) { + return _then(_AbsoluteLattePosition( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,center: null == center ? _self.center : center // ignore: cast_nullable_to_non_nullable +as Offset,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as double,radius: null == radius ? _self.radius : radius // ignore: cast_nullable_to_non_nullable +as double,squishAngle: freezed == squishAngle ? _self.squishAngle : squishAngle // ignore: cast_nullable_to_non_nullable +as double?,speed: null == speed ? _self.speed : speed // ignore: cast_nullable_to_non_nullable +as double,lastCollisionAt: freezed == lastCollisionAt ? _self.lastCollisionAt : lastCollisionAt // ignore: cast_nullable_to_non_nullable +as DateTime?,lastCollidedWith: freezed == lastCollidedWith ? _self.lastCollidedWith : lastCollidedWith // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +/// @nodoc +mixin _$LattePosition { + +/// Unique identifier to track collision pairs. + String get id;/// The center of the latte image. + Offset get center;/// The angle of this Latte's movement in radians. + double get direction;/// Size of the latte image as a percentage of the most constrained +/// dimension of the available space. A value of 1.0 would indicate a latte. + double get radius;/// The angle of the squish effect. + double? get squishAngle;/// The raw speed as calculated off of the last collision. If this is +/// greater than [defaultSpeed], then friction will slow the latte down +/// until it returns to [defaultSpeed] + double get rawSpeed;/// The last time this latte was involved in a collision. This is used to +/// calculate friction to slow down a circle's velocity. + DateTime? get lastCollisionAt;/// The ID of the last object this collided with to prevent rapid duplicate +/// collisions. + String? get lastCollidedWith; +/// Create a copy of LattePosition +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LattePositionCopyWith get copyWith => _$LattePositionCopyWithImpl(this as LattePosition, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LattePosition&&(identical(other.id, id) || other.id == id)&&(identical(other.center, center) || other.center == center)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.radius, radius) || other.radius == radius)&&(identical(other.squishAngle, squishAngle) || other.squishAngle == squishAngle)&&(identical(other.rawSpeed, rawSpeed) || other.rawSpeed == rawSpeed)&&(identical(other.lastCollisionAt, lastCollisionAt) || other.lastCollisionAt == lastCollisionAt)&&(identical(other.lastCollidedWith, lastCollidedWith) || other.lastCollidedWith == lastCollidedWith)); +} + + +@override +int get hashCode => Object.hash(runtimeType,id,center,direction,radius,squishAngle,rawSpeed,lastCollisionAt,lastCollidedWith); + +@override +String toString() { + return 'LattePosition(id: $id, center: $center, direction: $direction, radius: $radius, squishAngle: $squishAngle, rawSpeed: $rawSpeed, lastCollisionAt: $lastCollisionAt, lastCollidedWith: $lastCollidedWith)'; +} + + +} + +/// @nodoc +abstract mixin class $LattePositionCopyWith<$Res> { + factory $LattePositionCopyWith(LattePosition value, $Res Function(LattePosition) _then) = _$LattePositionCopyWithImpl; +@useResult +$Res call({ + String id, Offset center, double direction, double radius, double? squishAngle, double rawSpeed, DateTime? lastCollisionAt, String? lastCollidedWith +}); + + + + +} +/// @nodoc +class _$LattePositionCopyWithImpl<$Res> + implements $LattePositionCopyWith<$Res> { + _$LattePositionCopyWithImpl(this._self, this._then); + + final LattePosition _self; + final $Res Function(LattePosition) _then; + +/// Create a copy of LattePosition +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? center = null,Object? direction = null,Object? radius = null,Object? squishAngle = freezed,Object? rawSpeed = null,Object? lastCollisionAt = freezed,Object? lastCollidedWith = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,center: null == center ? _self.center : center // ignore: cast_nullable_to_non_nullable +as Offset,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as double,radius: null == radius ? _self.radius : radius // ignore: cast_nullable_to_non_nullable +as double,squishAngle: freezed == squishAngle ? _self.squishAngle : squishAngle // ignore: cast_nullable_to_non_nullable +as double?,rawSpeed: null == rawSpeed ? _self.rawSpeed : rawSpeed // ignore: cast_nullable_to_non_nullable +as double,lastCollisionAt: freezed == lastCollisionAt ? _self.lastCollisionAt : lastCollisionAt // ignore: cast_nullable_to_non_nullable +as DateTime?,lastCollidedWith: freezed == lastCollidedWith ? _self.lastCollidedWith : lastCollidedWith // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [LattePosition]. +extension LattePositionPatterns on LattePosition { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LattePosition value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LattePosition() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LattePosition value) $default,){ +final _that = this; +switch (_that) { +case _LattePosition(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LattePosition value)? $default,){ +final _that = this; +switch (_that) { +case _LattePosition() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, Offset center, double direction, double radius, double? squishAngle, double rawSpeed, DateTime? lastCollisionAt, String? lastCollidedWith)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LattePosition() when $default != null: +return $default(_that.id,_that.center,_that.direction,_that.radius,_that.squishAngle,_that.rawSpeed,_that.lastCollisionAt,_that.lastCollidedWith);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, Offset center, double direction, double radius, double? squishAngle, double rawSpeed, DateTime? lastCollisionAt, String? lastCollidedWith) $default,) {final _that = this; +switch (_that) { +case _LattePosition(): +return $default(_that.id,_that.center,_that.direction,_that.radius,_that.squishAngle,_that.rawSpeed,_that.lastCollisionAt,_that.lastCollidedWith);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, Offset center, double direction, double radius, double? squishAngle, double rawSpeed, DateTime? lastCollisionAt, String? lastCollidedWith)? $default,) {final _that = this; +switch (_that) { +case _LattePosition() when $default != null: +return $default(_that.id,_that.center,_that.direction,_that.radius,_that.squishAngle,_that.rawSpeed,_that.lastCollisionAt,_that.lastCollidedWith);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _LattePosition extends LattePosition { + const _LattePosition({required this.id, required this.center, required this.direction, required this.radius, required this.squishAngle, this.rawSpeed = LattePosition.defaultSpeed, this.lastCollisionAt, this.lastCollidedWith}): super._(); + + +/// Unique identifier to track collision pairs. +@override final String id; +/// The center of the latte image. +@override final Offset center; +/// The angle of this Latte's movement in radians. +@override final double direction; +/// Size of the latte image as a percentage of the most constrained +/// dimension of the available space. A value of 1.0 would indicate a latte. +@override final double radius; +/// The angle of the squish effect. +@override final double? squishAngle; +/// The raw speed as calculated off of the last collision. If this is +/// greater than [defaultSpeed], then friction will slow the latte down +/// until it returns to [defaultSpeed] +@override@JsonKey() final double rawSpeed; +/// The last time this latte was involved in a collision. This is used to +/// calculate friction to slow down a circle's velocity. +@override final DateTime? lastCollisionAt; +/// The ID of the last object this collided with to prevent rapid duplicate +/// collisions. +@override final String? lastCollidedWith; + +/// Create a copy of LattePosition +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LattePositionCopyWith<_LattePosition> get copyWith => __$LattePositionCopyWithImpl<_LattePosition>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LattePosition&&(identical(other.id, id) || other.id == id)&&(identical(other.center, center) || other.center == center)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.radius, radius) || other.radius == radius)&&(identical(other.squishAngle, squishAngle) || other.squishAngle == squishAngle)&&(identical(other.rawSpeed, rawSpeed) || other.rawSpeed == rawSpeed)&&(identical(other.lastCollisionAt, lastCollisionAt) || other.lastCollisionAt == lastCollisionAt)&&(identical(other.lastCollidedWith, lastCollidedWith) || other.lastCollidedWith == lastCollidedWith)); +} + + +@override +int get hashCode => Object.hash(runtimeType,id,center,direction,radius,squishAngle,rawSpeed,lastCollisionAt,lastCollidedWith); + +@override +String toString() { + return 'LattePosition(id: $id, center: $center, direction: $direction, radius: $radius, squishAngle: $squishAngle, rawSpeed: $rawSpeed, lastCollisionAt: $lastCollisionAt, lastCollidedWith: $lastCollidedWith)'; +} + + +} + +/// @nodoc +abstract mixin class _$LattePositionCopyWith<$Res> implements $LattePositionCopyWith<$Res> { + factory _$LattePositionCopyWith(_LattePosition value, $Res Function(_LattePosition) _then) = __$LattePositionCopyWithImpl; +@override @useResult +$Res call({ + String id, Offset center, double direction, double radius, double? squishAngle, double rawSpeed, DateTime? lastCollisionAt, String? lastCollidedWith +}); + + + + +} +/// @nodoc +class __$LattePositionCopyWithImpl<$Res> + implements _$LattePositionCopyWith<$Res> { + __$LattePositionCopyWithImpl(this._self, this._then); + + final _LattePosition _self; + final $Res Function(_LattePosition) _then; + +/// Create a copy of LattePosition +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? center = null,Object? direction = null,Object? radius = null,Object? squishAngle = freezed,Object? rawSpeed = null,Object? lastCollisionAt = freezed,Object? lastCollidedWith = freezed,}) { + return _then(_LattePosition( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,center: null == center ? _self.center : center // ignore: cast_nullable_to_non_nullable +as Offset,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as double,radius: null == radius ? _self.radius : radius // ignore: cast_nullable_to_non_nullable +as double,squishAngle: freezed == squishAngle ? _self.squishAngle : squishAngle // ignore: cast_nullable_to_non_nullable +as double?,rawSpeed: null == rawSpeed ? _self.rawSpeed : rawSpeed // ignore: cast_nullable_to_non_nullable +as double,lastCollisionAt: freezed == lastCollisionAt ? _self.lastCollisionAt : lastCollisionAt // ignore: cast_nullable_to_non_nullable +as DateTime?,lastCollidedWith: freezed == lastCollidedWith ? _self.lastCollidedWith : lastCollidedWith // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/models.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/models.dart new file mode 100644 index 0000000..5055571 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/models.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'latte_position.dart'; +export 'wobble_calculator.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/wobble_calculator.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/wobble_calculator.dart new file mode 100644 index 0000000..133feaf --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/models/wobble_calculator.dart @@ -0,0 +1,74 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/core/utils/utils.dart'; + +/// Calculates the strength of a wobble at a given time. +class WobbleCalculator { + /// Creates a new [WobbleCalculator]. + const WobbleCalculator({ + required this.wobblesPerContact, + }); + + /// Number of phases of wobble to animate through during contact. + /// + /// A value of 1 should result in the circle squishing once and then + /// returning to normal. A value of 2 should result in the circle squishing + /// in the initial direction, swinging back into a perpendicular squish of + /// reduced amplitude, then resetting to normal. A value of 3 should result + /// in an initial squish, perpendicular squish at a reduced amplitude, then + /// original squish again at yet another reduced amplitude, before settling + /// back to normal. + /// + /// Etc etc. + final int wobblesPerContact; + + /// Calculates the strength of a wobble at a given time. + Wobble getWobble(double t) { + final phase = t.phaseWithinRange(wobblesPerContact, 1); + final progressThroughPhase = t.progressThroughPhase( + wobblesPerContact, + 1, + phase: phase, + ); + + // Each slot should imply a dampening of the strength. + final dampener = 1 - (phase / (wobblesPerContact + 1)); + + late double strength; + // Odd slots are moving in the direction of the squish and so move from + // strength 0 to 1 for the first 50%, then back from 1 to 0. + if (progressThroughPhase < 0.5) { + strength = progressThroughPhase * 2 * dampener; + } else { + strength = (1 - progressThroughPhase) * 2 * dampener; + } + + return Wobble( + strength: strength, + isPerpendicular: phase.isEven, + ); + } +} + +/// Defines the direction and strength the wobble of a widget's squish effect +/// after colliding with another object. +class Wobble { + /// Creates a new [Wobble]. + Wobble({ + required this.strength, + required this.isPerpendicular, + }); + + /// The strength of the wobble, ranging from 0 to 1. + final double strength; + + /// Whether the wobble is perpendicular to the initial squish. + final bool isPerpendicular; + + @override + String toString() => + 'Wobble(strength: $strength: ' + 'isPerpendicular: $isPerpendicular)'; +} diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/recent_orders.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/recent_orders.dart new file mode 100644 index 0000000..f06cd67 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/recent_orders.dart @@ -0,0 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'home/recent_orders_home.dart'; +export 'models/models.dart'; +export 'widgets/widgets.dart'; diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/CONTEXT.md b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/CONTEXT.md new file mode 100644 index 0000000..4ed50b9 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/CONTEXT.md @@ -0,0 +1,25 @@ +# Recent Orders Widgets + +**Purpose:** +Provides the highly customized, visual effect components and animated containers utilized by the physics-engine driven "Recent Orders" screen. + +**Detailed File Overviews:** + +- `widgets.dart`: + - **Description**: Barrel export file. + +- `squishable_latte_image.dart`: + - **Description**: A customized image container. + - **Core Logic**: Wraps `Image.network` into a perfectly circular `ClipOval` but utilizes a responsive `Padding` box structure that allows the inner canvas to warp when provided a collision angle and velocity via `SquishedWidget`. + +- `squished_widget.dart`: + - **Description**: The mathematical deformation transformer. + - **Core Logic**: Applies a `Matrix4.identity()` mapping and dynamically scales the X and Y axes asynchronously to flatten the image oval along an implicit tangent line. + +- `thanos_snappable.dart`: + - **Description**: The particle exit animation controller. + - **Core Logic**: An intensely complex custom painting hook used when a bubble is pushed out of the "Recent 12" queue due to an inbound array length change. Uses a custom RenderBox shader approach (or equivalent particle emitter logic) to dissolve the image into dust. + +**Dependencies/Relationships:** +- Pure aesthetic wrappers consumed strictly by the `recent_orders_home_view.dart` loop. +- Consumes constraints configured inside `recent_orders_models`. diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/squishable_latte_image.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/squishable_latte_image.dart new file mode 100644 index 0000000..031dd03 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/squishable_latte_image.dart @@ -0,0 +1,156 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; +import 'package:genlatte/src/screens/recent_orders/models/models.dart'; +import 'package:genlatte/src/screens/recent_orders/widgets/widgets.dart'; +import 'package:genlatte/src/widgets/latte_image.dart' show LatteImageWidget; +import 'package:genlatte_data/models.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// A widget that displays a latte image that floats around the screen. +class SquishableLatteImage extends StatefulWidget { + /// Instantiates a [SquishableLatteImage]. + const SquishableLatteImage({ + required this.image, + required this.lattePosition, + this.paddingPercentage = 1, + this.wobblesPerContact = 6, + this.contactDuration = const Duration(milliseconds: 2000), + this.dampener = 0.3, + super.key, + }); + + /// The image to display. + final RecentLatteImage image; + + /// The position, size, direction, and speed of the latte image in pixels. + final AbsoluteLattePosition lattePosition; + + /// Number of phases of wobble to animate through during contact. + /// + /// A value of 1 should result in the circle squishing once and then + /// returning to normal. A value of 2 should result in the circle squishing + /// in the initial direction, swinging back into a perpendicular squish of + /// reduced amplitude, then resetting to normal. A value of 3 should result + /// in an initial squish, perpendicular squish at a reduced amplitude, then + /// original squish again at yet further reduced amplitude, before settling + /// back to normal. + /// + /// Etc etc. + final int wobblesPerContact; + + /// Percentage of default latte padding to be used, as represented via a + /// normalized double. + /// + /// A value of 1 is full padding to support the wobble animation, whereas a + /// value of 0 is zero padding, which will increase the rendered size of the + /// image. + final double paddingPercentage; + + /// The duration of the wobble animation. + final Duration contactDuration; + + /// Overall reduction of the wobble amplitude. Should be between 0 and 1. + final double dampener; + + @override + State createState() => _SquishableLatteImageState(); +} + +class _SquishableLatteImageState extends State + with TickerProviderStateMixin { + late final AnimationController _wobbleController; + + /// Vector of impact which is applying a squish to the image. + double? _squishAngle; + + late final WobbleCalculator _wobbleCalculator; + + @override + void initState() { + super.initState(); + + _wobbleController = + AnimationController( + vsync: this, + duration: widget.contactDuration, + ) + ..addListener(() { + if (!mounted) return; + setState(() {}); + }) + ..addStatusListener((status) { + if (!mounted) return; + if (status == AnimationStatus.completed) { + setState(() { + _squishAngle = null; + }); + } + }); + + _wobbleCalculator = WobbleCalculator( + wobblesPerContact: widget.wobblesPerContact, + ); + } + + @override + void didUpdateWidget(covariant SquishableLatteImage oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.lattePosition.lastCollisionAt != + oldWidget.lattePosition.lastCollisionAt) { + _squishAngle = widget.lattePosition.squishAngle; + if (_squishAngle != null) { + _startWobbleAnimation(); + } + } + } + + @override + void dispose() { + _wobbleController.dispose(); + super.dispose(); + } + + void _startWobbleAnimation() { + _wobbleController + ..duration = widget.contactDuration + ..forward(from: 0).ignore(); + } + + @override + Widget build(BuildContext context) { + Widget child = SizedBox( + width: widget.lattePosition.radius * 3, + height: widget.lattePosition.radius * 3, + child: Stack( + fit: StackFit.expand, + children: [ + Padding( + padding: EdgeInsets.all( + widget.lattePosition.radius * 0.5 * widget.paddingPercentage, + ), + child: LatteImageWidget( + imageUrl: widget.image.imageUrl, + ), + ), + ], + ), + ); + + if (_squishAngle != null) { + final wobble = _wobbleCalculator.getWobble(_wobbleController.value); + child = SquishedWidget( + direction: !wobble.isPerpendicular + ? _squishAngle! + : _squishAngle! + (pi / 2), + effectStrength: wobble.strength * widget.dampener, + child: child, + ); + } + + return child; + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/squished_widget.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/squished_widget.dart new file mode 100644 index 0000000..a112a30 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/squished_widget.dart @@ -0,0 +1,56 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_shaders/flutter_shaders.dart'; + +/// {@template SquishedWidget} +/// {@endtemplate} +class SquishedWidget extends StatelessWidget { + /// {@macro SquishedWidget} + const SquishedWidget({ + required this.effectStrength, + required this.direction, + required this.child, + super.key, + }); + + /// Widget to squish. + final Widget child; + + /// Direction of the force squishing the widget in radians. + final double direction; + + /// Strength of the force squishing the widget. The value should be between + /// -1 and 1, with a value of 0 indicating no squish. + final double effectStrength; + + @override + Widget build(BuildContext context) { + return ShaderBuilder( + (context, shader, innerChild) { + return AnimatedSampler( + (image, size, canvas) { + shader + ..setFloatUniforms((u) { + u + ..setSize(size) + ..setFloat(direction) + // The widget accepts a value of -1 to 1, but the shader + // itself is written to look best between -0.5 and 0.5, so + // we need to divide the strength by 2. + ..setFloat(effectStrength / 2); + }) + ..setImageSampler(0, image); + + canvas.drawRect(Offset.zero & size, Paint()..shader = shader); + }, + child: innerChild!, + ); + }, + assetKey: 'assets/shaders/squish.glsl', + child: child, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/thanos_snappable.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/thanos_snappable.dart new file mode 100644 index 0000000..0d705e8 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/thanos_snappable.dart @@ -0,0 +1,127 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: implementation_imports + +import 'package:flutter/material.dart'; +import 'package:thanos_snap_effect/src/shader_builder.dart'; +import 'package:thanos_snap_effect/src/shader_painter.dart'; +import 'package:thanos_snap_effect/src/shader_x/thanos_effect_shader.dart'; +import 'package:thanos_snap_effect/src/snappable_style.dart'; +import 'package:thanos_snap_effect/src/snapshot/snapshot_builder.dart'; + +/// A widget that can be snapped using the Thanos snap effect. +class ThanosSnappable extends StatefulWidget { + /// {@macro ThanosSnappable} + const ThanosSnappable({ + required this.onComplete, + required this.child, + super.key, + }); + + /// Widget to be snapped + final Widget child; + + /// Callback to be called when the snap animation is complete + final VoidCallback onComplete; + + @override + State createState() => _ThanosSnappableState(); +} + +class _ThanosSnappableState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 4), + ); + + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onComplete(); + } + }); + + // The SnapshotBuilder waits for a non-zero animation value to capture the + // widget. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.forward().ignore(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // We avoid using the Snappable widget from thanos_snap_effect because it + // uses OverlayPortal to pin the effect in one fixed place. By implementing + // our own builder here and rendering the shader directly in the tree, + // the vanishing "ghost" will follow the transform and position of this + // widget as it continues its momentum across the screen. + return ShaderBuilder( + shaderAsset: ThanosSnapEffectShader.path, + xShaderBuilder: ThanosSnapEffectShader.new, + builder: (context, shader, child) { + return SnapshotBuilder( + animation: _controller, + onSnapshotReadyListener: (snapshotInfo) { + shader?.updateSnapshot(snapshotInfo); + shader?.updateStyleProperties( + ThanosSnapEffectStyleProps.fromSnappableStyle( + const SnappableStyle(), + snapshotInfo, + ), + ); + }, + builder: (context, snapshotInfo, child) { + return Stack( + clipBehavior: Clip.none, + children: [ + Visibility( + maintainState: true, + maintainSize: true, + maintainAnimation: true, + // Keep the original image visible for the first ~80ms (0.02) + // of the snap. This seamlessly masks the 1-frame WebGL/Wasm + // shader compilation hitch that causes a split-second of + // 'nothing' before the shader is fully injected into the + // render pipeline. + visible: _controller.value < 0.02 || snapshotInfo == null, + child: child, + ), + if (snapshotInfo != null && shader != null) + AnimatedBuilder( + animation: _controller, + builder: (context, child) { + shader.setAnimationValue(_controller.value); + return Positioned.fill( + child: ShaderPainter( + shader: shader.fragmentShader, + outerPadding: const EdgeInsets.all(40), + animationValue: _controller.value, + ), + ); + }, + ), + ], + ); + }, + child: child, + ); + }, + child: widget.child, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/widgets.dart b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/widgets.dart new file mode 100644 index 0000000..d036682 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/screens/recent_orders/widgets/widgets.dart @@ -0,0 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'squishable_latte_image.dart'; +export 'squished_widget.dart'; +export 'thanos_snappable.dart'; diff --git a/genlatte/genlatte-ui/lib/src/sources/CONTEXT.md b/genlatte/genlatte-ui/lib/src/sources/CONTEXT.md new file mode 100644 index 0000000..24c2058 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/sources/CONTEXT.md @@ -0,0 +1,20 @@ +# External Sources + +**Purpose:** +Provides direct raw IO access boundaries to Cloud Infrastructure endpoints, preventing Vendor Lock-in by isolating exact Firebase APIs away from business logic repositories. + +**Detailed File Overviews:** + +- `sources.dart`: + - **Description**: Barrel export file. + +- `firestore_source.dart`: + - **Description**: The core NoSQL Database connection client. + - **Core Logic**: Interacts directly with the `cloud_firestore` plugin. Parses generic internal `Filter` tokens into Firebase-specific `Where` queries. Formats JSON maps directly into outbound streams using document snapshots. + +- `firestore_filters.dart`: + - **Description**: NoSQL Query Translator. + - **Core Logic**: Uses `freezed` to pattern-match repository filter requests and resolve them directly onto Firebase `Query` chains. + +**Dependencies/Relationships:** +- Strictly consumed by `genlatte-ui/lib/src/data` Repositories. diff --git a/genlatte/genlatte-ui/lib/src/sources/firestore_filters.dart b/genlatte/genlatte-ui/lib/src/sources/firestore_filters.dart new file mode 100644 index 0000000..037402f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/sources/firestore_filters.dart @@ -0,0 +1,44 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart' hide Filter; +import 'package:data_layer/data_layer.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'firestore_filters.freezed.dart'; +part 'firestore_filters.g.dart'; + +/// {@template FirestoreFilter} +/// Filter for Firestore queries. +/// {@endtemplate} +abstract class FirestoreFilter extends Filter { + /// {@macro FirestoreFilter} + const FirestoreFilter(); + + /// Add whatever WHERE clauses are necessary to modify this [query]. + Query apply(Query query); +} + +/// Firestore filters for queries against the Orders collection. +@Freezed() +sealed class OrdersFilter extends FirestoreFilter with _$OrdersFilter { + const OrdersFilter._(); + + /// Loads orders which are in any state other than incomplete. + const factory OrdersFilter.incompleteOrders() = IncompleteFilter; + + factory OrdersFilter.fromJson(Map json) => + _$OrdersFilterFromJson(json); + + @override + Query apply(Query query) => switch (this) { + IncompleteFilter() => query.where('status', isNotEqualTo: 'complete'), + }; + + @override + CacheKey get cacheKey => toString(); + + @override + Params toParams() => throw UnimplementedError(); +} diff --git a/genlatte/genlatte-ui/lib/src/sources/firestore_filters.freezed.dart b/genlatte/genlatte-ui/lib/src/sources/firestore_filters.freezed.dart new file mode 100644 index 0000000..d2186f5 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/sources/firestore_filters.freezed.dart @@ -0,0 +1,217 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'firestore_filters.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +OrdersFilter _$OrdersFilterFromJson( + Map json +) { + return IncompleteFilter.fromJson( + json + ); +} + +/// @nodoc +mixin _$OrdersFilter { + + + + /// Serializes this OrdersFilter to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is OrdersFilter); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'OrdersFilter()'; +} + + +} + +/// @nodoc +class $OrdersFilterCopyWith<$Res> { +$OrdersFilterCopyWith(OrdersFilter _, $Res Function(OrdersFilter) __); +} + + +/// Adds pattern-matching-related methods to [OrdersFilter]. +extension OrdersFilterPatterns on OrdersFilter { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( IncompleteFilter value)? incompleteOrders,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case IncompleteFilter() when incompleteOrders != null: +return incompleteOrders(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( IncompleteFilter value) incompleteOrders,}){ +final _that = this; +switch (_that) { +case IncompleteFilter(): +return incompleteOrders(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( IncompleteFilter value)? incompleteOrders,}){ +final _that = this; +switch (_that) { +case IncompleteFilter() when incompleteOrders != null: +return incompleteOrders(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function()? incompleteOrders,required TResult orElse(),}) {final _that = this; +switch (_that) { +case IncompleteFilter() when incompleteOrders != null: +return incompleteOrders();case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function() incompleteOrders,}) {final _that = this; +switch (_that) { +case IncompleteFilter(): +return incompleteOrders();} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function()? incompleteOrders,}) {final _that = this; +switch (_that) { +case IncompleteFilter() when incompleteOrders != null: +return incompleteOrders();case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class IncompleteFilter extends OrdersFilter { + const IncompleteFilter(): super._(); + factory IncompleteFilter.fromJson(Map json) => _$IncompleteFilterFromJson(json); + + + + +@override +Map toJson() { + return _$IncompleteFilterToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is IncompleteFilter); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'OrdersFilter.incompleteOrders()'; +} + + +} + + + + +// dart format on diff --git a/genlatte/genlatte-ui/lib/src/sources/firestore_filters.g.dart b/genlatte/genlatte-ui/lib/src/sources/firestore_filters.g.dart new file mode 100644 index 0000000..2e45812 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/sources/firestore_filters.g.dart @@ -0,0 +1,17 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'firestore_filters.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +IncompleteFilter _$IncompleteFilterFromJson(Map json) => + IncompleteFilter(); + +Map _$IncompleteFilterToJson(IncompleteFilter instance) => + {}; diff --git a/genlatte/genlatte-ui/lib/src/sources/firestore_source.dart b/genlatte/genlatte-ui/lib/src/sources/firestore_source.dart new file mode 100644 index 0000000..dad30dc --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/sources/firestore_source.dart @@ -0,0 +1,459 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart' hide Source; +import 'package:data_layer/data_layer.dart'; +import 'package:genlatte/src/sources/firestore_filters.dart'; +import 'package:logging/logging.dart'; +import 'package:stream_transform/stream_transform.dart'; + +/// {@template FirestoreSource} +/// Firestore implementation of [Source]. +/// {@endtemplate} +class FirestoreSource extends Source with WatchableSource { + /// {@macro FirestoreSource} + FirestoreSource( + this.firestore, { + required super.bindings, + this.onCreateServerTimestampFields = const [], + this.onUpdateServerTimestampFields = const [], + }) { + _log = Logger('FirestoreSource<$T>::${bindings.getListUrl().path}'); + } + + /// Firestore, baby! + final FirebaseFirestore firestore; + + /// Fields that should be set to [FieldValue.serverTimestamp()] on create. + final List onCreateServerTimestampFields; + + /// Fields that should be set to [FieldValue.serverTimestamp()] on every write + final List onUpdateServerTimestampFields; + + /// The Firestore collection for this source. + CollectionReference get collection => + _collection ??= firestore.collection(bindings.getListUrl().path); + + CollectionReference? _collection; + + late final Logger _log; + + @override + Future> getById(ReadOperation operation) => + _guarded(() => _getById(operation), operation); + + Future> _getById(ReadOperation operation) => + collection.doc(operation.itemId).get().then((snapshot) { + if (!snapshot.exists) { + return ReadSuccess(null, details: operation.details); + } + return ReadSuccess( + bindings.fromJson( + cleanData(snapshot.data() ?? {})..addAll({'id': snapshot.id}), + ), + details: operation.details, + ); + }); + + @override + Future> getByIds(ReadByIdsOperation operation) => + _guarded(() => _getByIds(operation), operation); + + Future> _getByIds(ReadByIdsOperation operation) async { + if (operation.itemIds.length <= 30) { + return collection + .where(FieldPath.documentId, whereIn: operation.itemIds) + .get() + .then((snapshot) => _processSnapshotDocs(snapshot.docs, operation)); + } + + final chunks = operation.itemIds.toList().chunks(30).toList(); + final snapshots = await Future.wait( + chunks.map( + (chunk) => collection.where(FieldPath.documentId, whereIn: chunk).get(), + ), + ); + + final allDocs = snapshots.expand((s) => s.docs).toList(); + return _processSnapshotDocs(allDocs, operation); + } + + ReadListResult _processSnapshotDocs( + List> docs, + ReadByIdsOperation operation, + ) { + final items = docs + .map( + (doc) => bindings.fromJson( + cleanData(doc.data())..addAll({'id': doc.id}), + ), + ) + .toList(); + final missingIds = operation.itemIds.toSet().difference( + docs.map((doc) => doc.id).toSet(), + ); + return ReadListResult.fromList( + items, + operation.details, + missingIds, + bindings.getId, + ); + } + + @override + Future> getItems(ReadListOperation operation) => + _guarded(() => _getItems(operation), operation); + + Future> _getItems(ReadListOperation operation) { + Query query = collection; + if (operation.details.filter != null) { + if (operation.details.filter is! FirestoreFilter) { + throw UnsupportedError( + 'Filter ${operation.details.filter.runtimeType} is not supported', + ); + } + query = (operation.details.filter! as FirestoreFilter).apply(query); + } + return query.get().then((snapshot) { + return ReadListResult.fromList( + snapshot.docs + .map( + (doc) => bindings.fromJson( + cleanData(doc.data())..addAll({'id': doc.id}), + ), + ) + .toList(), + operation.details, + {}, + bindings.getId, + ); + }); + } + + @override + Future> setItem(WriteOperation operation) => + _guarded(() => _setItem(operation), operation); + + Future> _setItem(WriteOperation operation) { + final existingId = bindings.getId(operation.item); + var dataToWrite = bindings.toJson(operation.item)..remove('id'); + + // Apply server timestamps for updates + for (final field in onUpdateServerTimestampFields) { + dataToWrite[field] = FieldValue.serverTimestamp(); + } + if (existingId == null || operation.details.forceInsert) { + // Apply server timestamps for create + for (final field in onCreateServerTimestampFields) { + dataToWrite[field] = FieldValue.serverTimestamp(); + } + } + + dataToWrite = cleanDataForWrite(dataToWrite); + + if (existingId == null && !operation.details.forceInsert) { + return collection.add(dataToWrite).then( + (doc) async { + final snapshot = await doc.get(); + return WriteSuccess( + bindings.fromJson( + cleanData(snapshot.data() ?? {})..addAll({'id': snapshot.id}), + ), + details: operation.details, + ); + }, + ); + } + + if (existingId == null && operation.details.forceInsert) { + throw UnsupportedError('Cannot force insert with no id'); + } + + final snapshot = collection.doc(existingId); + final writeFuture = operation.details.forceInsert + ? snapshot.set(dataToWrite) + : snapshot.update(dataToWrite); + + return writeFuture.then( + (_) { + return WriteSuccess( + bindings.fromJson(bindings.toJson(operation.item)), + details: operation.details, + ); + }, + ); + } + + @override + Future> setItems(WriteListOperation operation) => + throw UnimplementedError(); + + @override + Future> delete(DeleteOperation operation) { + return _guarded(() => _delete(operation), operation); + } + + Future> _delete(DeleteOperation operation) { + return collection + .doc(operation.itemId) + .delete() + .then( + (_) => DeleteSuccess(operation.details), + ); + } + + @override + SourceType sourceType = SourceType.remote; + + @override + Stream> watch(ReadOperation operation) => + _guardedSync(() => _watch(operation), operation); + + Stream> _watch(ReadOperation operation) { + return collection.doc(operation.itemId).snapshots().map((snapshot) { + if (!snapshot.exists) { + return ReadSuccess(null, details: operation.details); + } + return ReadSuccess( + bindings.fromJson( + cleanData(snapshot.data() ?? {})..addAll({'id': snapshot.id}), + ), + details: operation.details, + ); + }); + } + + @override + Stream> watchByIds(ReadByIdsOperation operation) => + _guardedSync(() => _watchByIds(operation), operation); + + Stream> _watchByIds(ReadByIdsOperation operation) { + if (operation.itemIds.length <= 30) { + return collection + .where(FieldPath.documentId, whereIn: operation.itemIds) + .snapshots() + .map((snapshot) => _processSnapshotDocs(snapshot.docs, operation)); + } + + final chunks = operation.itemIds.toList().chunks(30).toList(); + final streams = chunks.map( + (chunk) => collection + .where(FieldPath.documentId, whereIn: chunk) + .snapshots() + .map((s) => s.docs), + ); + + final streamsList = streams.toList(); + return streamsList.first + .combineLatestAll(streamsList.skip(1)) + .map( + (docsList) => _processSnapshotDocs( + docsList.expand((docs) => docs).toList(), + operation, + ), + ); + } + + @override + Stream> watchList(ReadListOperation operation) => + _guardedSync(() => _watchList(operation), operation); + + Stream> _watchList(ReadListOperation operation) { + Query query = collection; + if (operation.details.filter != null) { + if (operation.details.filter is! FirestoreFilter) { + throw UnsupportedError( + 'Filter ${operation.details.filter.runtimeType} is not supported', + ); + } + query = (operation.details.filter! as FirestoreFilter).apply(query); + } + return query.snapshots().map((snapshot) { + return ReadListResult.fromList( + snapshot.docs + .map( + (doc) => bindings.fromJson( + cleanData(doc.data())..addAll({'id': doc.id}), + ), + ) + .toList(), + operation.details, + {}, + bindings.getId, + ); + }); + } + + /// Converts Firebase [Timestamp] values into Dart [DateTime]s. + static Json cleanData(Json data) { + if (data.isEmpty) { + return data; + } + + Json? cleaned; + + for (final entry in data.entries) { + final value = entry.value; + final cleanedValue = _cleanValue(value); + + if (!identical(cleanedValue, value)) { + cleaned ??= Map.from(data); + cleaned[entry.key] = cleanedValue; + } + } + + return cleaned ?? data; + } + + static Object? _cleanValue(Object? value) { + if (value is Timestamp) { + return value.toDate().toUtc().toIso8601String(); + } + if (value is Map) { + return cleanData(value); + } + if (value is List) { + List? newList; + for (var i = 0; i < value.length; i++) { + final item = value[i]; + final cleanedItem = _cleanValue(item); + if (!identical(cleanedItem, item)) { + newList ??= List.from(value); + newList[i] = cleanedItem; + } + } + return newList ?? value; + } + return value; + } + + /// Converts Dart [DateTime]s into Firebase [Timestamp] values. + static Json cleanDataForWrite(Json data) { + if (data.isEmpty) { + return data; + } + + Json? cleaned; + + for (final entry in data.entries) { + final value = entry.value; + final cleanedValue = _cleanValueForWrite(value); + + if (!identical(cleanedValue, value)) { + cleaned ??= Map.from(data); + cleaned[entry.key] = cleanedValue; + } + } + + return cleaned ?? data; + } + + static Object? _cleanValueForWrite(Object? value) { + if (value is DateTime) { + return Timestamp.fromDate(value); + } + if (value is String) { + final iso8601Regex = RegExp( + r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z)?$', + ); + if (iso8601Regex.hasMatch(value)) { + final parsed = DateTime.tryParse(value); + if (parsed != null) { + return Timestamp.fromDate(parsed); + } + } + } + if (value is Map) { + return cleanDataForWrite(value); + } + if (value is List) { + List? newList; + for (var i = 0; i < value.length; i++) { + final item = value[i]; + final cleanedItem = _cleanValueForWrite(item); + if (!identical(cleanedItem, item)) { + newList ??= List.from(value); + newList[i] = cleanedItem; + } + } + return newList ?? value; + } + return value; + } + + Future _guarded(Future Function() fn, Operation operation) async { + try { + return await fn(); + } on FirebaseException catch (e) { + _handleFirebaseException(e, operation); + rethrow; + } on Exception catch (e) { + final description = _describeOperation(operation); + _log.severe('Uncaught error: $e. $description'); + rethrow; + } + } + + R _guardedSync(R Function() fn, Operation operation) { + try { + return fn(); + } on FirebaseException catch (e) { + _handleFirebaseException(e, operation); + rethrow; + } on Exception catch (e) { + final description = _describeOperation(operation); + _log.severe('Uncaught error: $e. $description'); + rethrow; + } + } + + void _handleFirebaseException( + FirebaseException e, + Operation operation, + ) { + final description = _describeOperation(operation); + switch (e.code) { + case 'permission-denied': + _log.severe('Permission denied: ${e.message}. $description'); + case 'not-found': + _log.severe('Not found: ${e.message}. $description'); + case 'unavailable': + _log.severe('The service is currently unavailable (offline?).'); + case 'unauthenticated': + _log.severe('User must be logged in to perform this action.'); + case 'deadline-exceeded': + _log.severe('The operation took too long to complete.'); + default: + _log.severe( + 'Uncaught Firebase error ${e.code} :: ${e.message}. $description', + ); + } + } + + String _describeOperation(Operation operation) { + final collectionName = bindings.getListUrl().path; + return switch (operation) { + ReadOperation() => + 'Failed to read $collectionName/${operation.itemId}', + ReadByIdsOperation() => + 'Failed to read $collectionName/${operation.itemIds}', + ReadListOperation() => 'Failed to read $collectionName', + WriteOperation() => + 'Failed to write $collectionName/${bindings.getId(operation.item)}', + WriteListOperation() => 'Failed to write $collectionName', + DeleteOperation() => + 'Failed to delete $collectionName/${operation.itemId}', + }; + } +} + +extension _ListChunkX on List { + Iterable> chunks(int size) sync* { + for (var i = 0; i < length; i += size) { + yield sublist(i, i + size > length ? length : i + size); + } + } +} diff --git a/genlatte/genlatte-ui/lib/src/sources/sources.dart b/genlatte/genlatte-ui/lib/src/sources/sources.dart new file mode 100644 index 0000000..e937776 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/sources/sources.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'firestore_filters.dart'; +export 'firestore_source.dart'; diff --git a/genlatte/genlatte-ui/lib/src/widgets/CONTEXT.md b/genlatte/genlatte-ui/lib/src/widgets/CONTEXT.md new file mode 100644 index 0000000..e920f5f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/CONTEXT.md @@ -0,0 +1,27 @@ +# Common Widgets + +**Purpose:** +Hosts shared UI primitives and highly complex layout mathematics structures that are reused globally across the application. + +**Detailed File Overviews:** + +- `widgets.dart`: + - **Description**: Barrel export file. + +- `buttons.dart` / `chevron_button.dart` / `dynamic_button_layout.dart`: + - **Description**: Custom Interactive elements. + - **Core Logic**: Reusable touch targets overriding defaults to provide deep haptics, stylized borders, or specific branding physics (e.g. `ChevronAnimatedButton`). + +- `genlatte_scaffold.dart.dart` / `configuration_card.dart` / `footer.dart`: + - **Description**: Core Structural components. + - **Core Logic**: The root branding skeletons framing the views, providing headers, progress indicators, or standard card backgrounds. + +- `design_scalar.dart` / `responsive_sized_box.dart` / `layout_provider.dart`: + - **Description**: Scaling Math logic. + - **Core Logic**: Complex `LayoutBuilder` implementations acting as alternatives to standard media queries, capable of perfectly scaling Typography and Widget padding based on percentage of available constraints (critical for resizing between Mobile, Tablet, and weird aspect ratio Kiosk monitors). + +- `triple_tap.dart`: + - **Description**: Advanced gesture recognition used to hide developer tools intentionally behind complex physical interactions. + +**Dependencies/Relationships:** +- Consumed widely throughout `genlatte-ui/lib/src/screens/`. diff --git a/genlatte/genlatte-ui/lib/src/widgets/animated_gradient.dart b/genlatte/genlatte-ui/lib/src/widgets/animated_gradient.dart new file mode 100644 index 0000000..55788e4 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/animated_gradient.dart @@ -0,0 +1,107 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:genlatte/src/screens/app/theme.dart'; + +class AnimatedGradientBackground extends StatelessWidget { + const AnimatedGradientBackground({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Base background + Container(color: AppColors.white), + + // Animated Blobs + _Blob( + color: AppColors.googleIntroBlue.withValues(alpha: 0.3), + begin: Alignment.topLeft, + end: Alignment.bottomRight, + duration: 10.seconds, + ), + _Blob( + color: AppColors.googleIntroRed.withValues(alpha: 0.2), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + duration: 8.seconds, + delay: 1.seconds, + ), + _Blob( + color: AppColors.googleIntroGreen.withValues(alpha: 0.2), + begin: Alignment.bottomLeft, + end: Alignment.topRight, + duration: 12.seconds, + delay: 2.seconds, + ), + _Blob( + color: AppColors.latteArtGold.withValues(alpha: 0.2), + begin: Alignment.centerRight, + end: Alignment.centerLeft, + duration: 15.seconds, + delay: 3.seconds, + ), + + // Blur effect to make it look like a mesh gradient + const IgnorePointer( + child: SizedBox.expand( + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.transparent, + ), + ), + ), + ), + ], + ); + } +} + +class _Blob extends StatelessWidget { + const _Blob({ + required this.color, + required this.begin, + required this.end, + required this.duration, + this.delay = Duration.zero, + }); + + final Color color; + final Alignment begin; + final Alignment end; + final Duration duration; + final Duration delay; + + @override + Widget build(BuildContext context) { + return Animate( + onPlay: (controller) => controller.repeat(reverse: true), + delay: delay, + ).custom( + duration: duration, + builder: (context, value, child) { + final alignment = Alignment.lerp(begin, end, value)!; + return Align( + alignment: alignment, + child: Container( + width: 400, + height: 400, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: color, + blurRadius: 150, + spreadRadius: 100, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/buttons.dart b/genlatte/genlatte-ui/lib/src/widgets/buttons.dart new file mode 100644 index 0000000..8c3cf01 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/buttons.dart @@ -0,0 +1,377 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Standard application button with slightly rounded corners, a thin border, +/// and a transparent background. +class GenLatteOutlinedButton extends StatelessWidget { + const GenLatteOutlinedButton._({ + required this.label, + required this.onPressed, + required _ButtonLuminence luminence, + required this.size, + super.key, + }) : _luminence = luminence; + + /// Creates a new [GenLatteOutlinedButton] with a white border, suitable to + /// pair with dark backgrounds. + factory GenLatteOutlinedButton.light({ + required String label, + required VoidCallback? onPressed, + GenLatteButtonSize size = .normal, + Key? key, + }) { + return GenLatteOutlinedButton._( + label: label, + onPressed: onPressed, + luminence: .light, + size: size, + key: key, + ); + } + + /// Creates a new [GenLatteOutlinedButton] with a red border, suitable for + /// urgent alerts. + factory GenLatteOutlinedButton.red({ + required String label, + required VoidCallback? onPressed, + GenLatteButtonSize size = .normal, + Key? key, + }) { + return GenLatteOutlinedButton._( + label: label, + onPressed: onPressed, + luminence: .red, + size: size, + key: key, + ); + } + + /// Creates a new [GenLatteOutlinedButton] with a dark border, suitable to + /// pair with light backgrounds. + factory GenLatteOutlinedButton.dark({ + required String label, + required VoidCallback? onPressed, + GenLatteButtonSize size = .normal, + Key? key, + }) { + return GenLatteOutlinedButton._( + label: label, + onPressed: onPressed, + luminence: .dark, + size: size, + key: key, + ); + } + + /// Text to show in the button. + final String label; + + /// Callback to invoke when the button is pressed. + final VoidCallback? onPressed; + + /// {@macro _ButtonLuminence} + final _ButtonLuminence _luminence; + + /// {@macro _ButtonSize} + final GenLatteButtonSize size; + + @override + Widget build(BuildContext context) { + final color = switch (_luminence) { + .light => const Color(0xFFFFFFFF), + .dark => const Color(0xFF000000), + .red => const Color(0xFFD32F2F), + }; + final padding = size == .normal + ? const EdgeInsets.symmetric(horizontal: 16, vertical: 12) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 16); + return Button( + onPressed: onPressed, + style: const ButtonStyle.outline() + .withPadding(padding: padding) + .withBackgroundColor( + color: Colors.transparent, + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + disabledColor: Colors.transparent, + ) + .withBorder( + border: Border.all(color: color), + hoverBorder: Border.all(color: color), + focusBorder: Border.all(color: color), + disabledBorder: Border.all(color: color), + ) + .withBorderRadius( + borderRadius: BorderRadius.circular(12), + hoverBorderRadius: BorderRadius.circular(12), + focusBorderRadius: BorderRadius.circular(12), + disabledBorderRadius: BorderRadius.circular(12), + ), + // There is a problem with the `withPadding()` method. The + // plumbing which eventually resolves button padding + child: Text( + label, + style: TextStyle( + color: color, + fontSize: switch (size) { + .normal => 14, + .large => 16, + }, + ), + ), + ); + } +} + +/// Similar to an outline button, but used for configuring a coffee or the +/// generated art which will live atop it. +class GenLatteConfigurationButton extends StatelessWidget { + /// Creates a new [GenLatteConfigurationButton]. + const GenLatteConfigurationButton({ + required this.label, + required this.isSelected, + required this.onPressed, + required this.isTight, + required this.uiScale, + super.key, + }); + + /// Text to show in the button. + final String label; + + /// Callback to invoke when the button is pressed. + final VoidCallback onPressed; + + /// Whether this button is selected. + final bool isSelected; + + /// If true, everything is a little smaller. + final bool isTight; + + /// Scaling up or down of the UI element. + final double uiScale; + + /// UI padding for tight buttons (before uiScaling). + static const tightPadding = EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ); + + /// UI padding for normal buttons (before uiScaling). + static const normalPadding = EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ); + + static final _tightBorderRadius = BorderRadius.circular(6); + static final _normalBorderRadius = BorderRadius.circular(8); + + @override + Widget build(BuildContext context) { + final borderRadius = + (isTight ? _tightBorderRadius : _normalBorderRadius) * uiScale; + final padding = (isTight ? tightPadding : normalPadding) * uiScale; + final backgroundColor = isSelected ? AppColors.black : Colors.transparent; + + // Default values are 1px black. + final border = Border.all(); + + return Button( + onPressed: onPressed, + style: const ButtonStyle.outline() + .withBackgroundColor( + color: backgroundColor, + hoverColor: backgroundColor, + focusColor: backgroundColor, + disabledColor: backgroundColor, + ) + .withBorder( + border: border, + hoverBorder: border, + focusBorder: border, + disabledBorder: border, + ) + .withBorderRadius( + borderRadius: borderRadius, + hoverBorderRadius: borderRadius, + focusBorderRadius: borderRadius, + disabledBorderRadius: borderRadius, + ) + .withPadding( + padding: padding, + hoverPadding: padding, + focusPadding: padding, + disabledPadding: padding, + ), + // There is a problem with the `withPadding()` method. The + // plumbing which eventually resolves button padding + child: Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : const Color(0xFF000000), + fontSize: 14 * uiScale, + ), + ), + ); + } +} + +/// Standard application button with slightly rounded corners and a filled white +/// background. +class GenLatteFilledButton extends StatelessWidget { + /// Creates a new [GenLatteFilledButton]. + const GenLatteFilledButton({ + required this.onPressed, + this.label, + this.icon, + this.trailingIcon, + this.size = .normal, + super.key, + }) : assert( + label != null || icon != null || trailingIcon != null, + 'Must provide either a label or an icon or trailingIcon.', + ); + + /// Text to show in the button. + final String? label; + + /// Icon to show in the button. + final IconData? icon; + + /// Icon to show after the label. + final IconData? trailingIcon; + + /// Callback to invoke when the button is pressed. + final VoidCallback onPressed; + + /// {@macro _ButtonSize} + final GenLatteButtonSize size; + + /// Maximum and default size of a button; used to scale down UI values when + /// this button finds itself in tighter quarters. + static const _defaultSize = 60; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final double uiScale = + (min(constraints.maxWidth, constraints.maxHeight) / _defaultSize) + .clamp(0.5, 1); + final padding = size == .normal + ? EdgeInsets.symmetric( + horizontal: 16 * uiScale, + vertical: 12 * uiScale, + ) + : EdgeInsets.symmetric( + horizontal: 16 * uiScale, + vertical: 16 * uiScale, + ); + final labelChild = label != null + ? Text( + label!, + style: TextStyle( + color: const Color(0xFF000000), + fontSize: 16 * uiScale, + ), + ) + : null; + final iconChild = icon != null + ? Icon(icon, size: 24 * uiScale, color: const Color(0xFF000000)) + : null; + + final trailingIconChild = trailingIcon != null + ? Icon( + trailingIcon, + size: 24 * uiScale, + color: const Color(0xFF000000), + ) + : null; + + // If we only have an icon, force an outright circular shape + final ButtonShape shape = icon != null && label == null + ? .circle + : .rectangle; + + final border = shape == .rectangle + ? Border.all(color: const Color(0x00000000)) + : null; + + final borderRadius = shape == .rectangle + ? BorderRadius.circular(999) + : null; + + var buttonStyle = ButtonStyle.outline(shape: shape) + .withPadding(padding: padding) + .withBackgroundColor(color: const Color(0xFFFFFFFF)); + + buttonStyle = buttonStyle.withBorder( + border: border, + hoverBorder: border, + focusBorder: border, + disabledBorder: border, + ); + + buttonStyle = buttonStyle.withBorderRadius( + borderRadius: borderRadius, + hoverBorderRadius: borderRadius, + focusBorderRadius: borderRadius, + disabledBorderRadius: borderRadius, + ); + + return Button( + onPressed: onPressed, + style: buttonStyle, + // This key is needed to force the button to rebuild when the shape + // changes. + // https://github.com/sunarya-thito/shadcn_flutter/issues/404 + key: ValueKey('button-$shape'), + child: Row( + mainAxisSize: .min, + children: [ + ?iconChild, + if (iconChild != null && labelChild != null) // + SizedBox(width: 8 * uiScale), + ?labelChild, + if (trailingIcon != null && labelChild != null) // + SizedBox(width: 8 * uiScale), + ?trailingIconChild, + ], + ), + ); + }, + ); + } +} + +/// {@template _ButtonLuminence} +/// Color flavor of a button. +/// {@endtemplate} +enum _ButtonLuminence { + /// Indicates a primarily white button, suitable for dark backgrounds. + light, + + /// Indicates a primarily dark button, suitable for light luminence + /// backgrounds. + dark, + + /// Indicates a red button, suitable for urgent alerts. + red, +} + +/// {@template ButtonSize} +/// Size of a button. +/// {@endtemplate} +enum GenLatteButtonSize { + /// Default button sizing + normal, + + /// Extra padding, for when it really matters + large, +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/chevron_button.dart b/genlatte/genlatte-ui/lib/src/widgets/chevron_button.dart new file mode 100644 index 0000000..6e0b738 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/chevron_button.dart @@ -0,0 +1,585 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/services.dart'; +import 'package:genlatte/src/screens/app/theme.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// The orientation of the chevron button. +enum ChevronButtonType { + /// A tall chevron button, suitable for rows within landscape UIs. + vertical, + + /// A short chevron button, suitable for columns within portrait UIs. + flat, +} + +/// A button that is shaped like a chevron pointing to the right. The two +/// shapes, `vertical` and `flat`, are for tablets/desktop and mobile +/// respectively. +class ChevronButton extends StatefulWidget { + /// Generic constructor for a [ChevronButton]. + ChevronButton._({ + required this.builder, + required ChevronButtonType type, + required this.color, + required this.width, + required this.height, + required this.onPressed, + required this.scale, + required this.textColor, + required this.textPadding, + this.borderColor, + super.key, + }) { + painterBuilder = type == ChevronButtonType.vertical + ? (Color color, Color? borderColor) => + _verticalPainter(color, borderColor, scale) + : (Color color, Color? borderColor) => + _flatPainter(color, borderColor, scale); + } + + /// Creates a [ChevronButton] with vertical orientation at the given scale. + factory ChevronButton.vertical({ + required Widget Function(BuildContext, double, Color?) builder, + VoidCallback? onPressed, + double scale = 1.0, + Color? color, + Color? borderColor, + Color? textColor, + Key? key, + }) => ChevronButton._( + builder: builder, + width: _VerticalChevronPainter._defaultWidth * scale, + height: _VerticalChevronPainter._defaultHeight * scale, + color: color ?? AppColors.chevronYellow, + borderColor: borderColor, + onPressed: onPressed, + scale: scale, + textColor: textColor, + textPadding: EdgeInsets.only( + left: (_VerticalChevronPainter._defaultArrowDepth * scale) * 0.98, + ), + type: .vertical, + key: key, + ); + + /// Creates a [ChevronButton] with flat orientation and the given scale. + factory ChevronButton.flat({ + required Widget Function(BuildContext, double, Color?) builder, + VoidCallback? onPressed, + double scale = 1.0, + Color? color, + Color? borderColor, + Color? textColor, + Key? key, + }) => ChevronButton._( + builder: builder, + width: _FlatChevronPainter._defaultWidth * scale, + height: _FlatChevronPainter._defaultHeight * scale, + color: color ?? AppColors.chevronYellow, + borderColor: borderColor, + onPressed: onPressed, + scale: scale, + textColor: textColor, + textPadding: EdgeInsets.zero, + type: .flat, + key: key, + ); + + static CustomPainter _verticalPainter( + Color color, + Color? borderColor, + double scale, + ) => _VerticalChevronPainter( + color: color, + borderColor: borderColor, + radius: _VerticalChevronPainter._defaultCornerRadius * scale, + depth: _VerticalChevronPainter._defaultArrowDepth * scale, + scale: scale, + ); + + static CustomPainter _flatPainter( + Color color, + Color? borderColor, + double scale, + ) => _FlatChevronPainter( + color: color, + borderColor: borderColor, + tailWidth: _FlatChevronPainter._defaultTailWidth * scale, + arrowWidth: _FlatChevronPainter._defaultArrowWidth * scale, + scale: scale, + ); + + /// Descendant builder with UI scaling. + final Widget Function( + BuildContext context, + double textScale, + Color? textColor, + ) + builder; + + /// Final width of the chevron. + final double width; + + /// Final height of the chevron. + final double height; + + /// Color of the chevron. + final Color color; + + /// Optional border color that defaults to the main color if omitted. + final Color? borderColor; + + /// UI scalar. + final double scale; + + /// Optional text color that defaults to the main color if omitted. + final Color? textColor; + + /// Special padding for the button text to make it look centered. + final EdgeInsets textPadding; + + /// Callback when the button is pressed. + final VoidCallback? onPressed; + + /// That which draws the button itself. + late final CustomPainter Function(Color, Color?) painterBuilder; + + @override + State createState() => _ChevronButtonState(); +} + +class _ChevronButtonState extends State { + bool _isHovered = false; + bool _isFocused = false; + + @override + Widget build(BuildContext context) { + // Darken the color slightly when hovered or focused + final effectiveColor = (widget.onPressed == null) + ? const Color(0xFF3D3D3D) + : (_isHovered || _isFocused) + ? Color.lerp(widget.color, Colors.black, 0.1)! + : widget.color; + + final effectiveBorderColor = widget.borderColor == null + ? null + : (widget.onPressed == null) + ? const Color(0xFF3D3D3D) + : (_isHovered || _isFocused) + ? Color.lerp(widget.borderColor, Colors.black, 0.1)! + : widget.borderColor; + + return Semantics( + button: true, + enabled: widget.onPressed != null, + onTap: widget.onPressed, + child: FocusableActionDetector( + mouseCursor: SystemMouseCursors.click, + onShowFocusHighlight: (value) => setState(() => _isFocused = value), + onShowHoverHighlight: (value) => setState(() => _isHovered = value), + actions: { + ActivateIntent: CallbackAction( + onInvoke: (_) => widget.onPressed?.call(), + ), + }, + shortcuts: const { + SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(), + SingleActivator(LogicalKeyboardKey.space): ActivateIntent(), + }, + child: GestureDetector( + onTap: widget.onPressed, + child: SizedBox( + width: widget.width, + height: widget.height, + child: Stack( + children: [ + Positioned.fill( + child: CustomPaint( + painter: widget.painterBuilder( + effectiveColor, + effectiveBorderColor, + ), + ), + ), + Center( + child: Padding( + padding: widget.textPadding, + child: widget.builder( + context, + widget.scale, + widget.textColor, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _VerticalChevronPainter extends CustomPainter { + _VerticalChevronPainter({ + required this.color, + required this.radius, + required this.depth, + required this.scale, + this.borderColor, + }); + + final Color color; + final Color? borderColor; + final double radius; + final double depth; + final double scale; + + static const double _defaultWidth = 214; + static const double _defaultHeight = 405; + static const double _aspectRatio = _defaultWidth / _defaultHeight; + static const Size _defaultSize = Size(_defaultWidth, _defaultHeight); + static const double _defaultCornerRadius = 20; + static const double _defaultArrowDepth = 27; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final w = size.width; + final h = size.height; + + // Calculate the vertical center for the chevron tip + final centerY = h / 2; + + // We add a tiny smoothing curve at the arrow tip so it's not razor sharp + const tipRadius = 4.0; + + final path = Path() + // 1. Start at Top-Left (after the corner) + // The shape is indented on the left, so x starts at 0 and goes IN by + // 'depth' + ..moveTo(0, radius) + // 2. Top-Left Corner + // We use ArcToPoint to ensure a perfect circular corner regardless of + // scale + ..arcToPoint( + Offset(radius, 0), + radius: Radius.circular(radius), + ) + // 3. Top Edge + // Goes to the right, but stops short for the right corner. + // The right side is shifted "out" by `depth`. + // The main body width is (w - depth). + ..lineTo(w - depth - radius, 0) + // 4. Top-Right Corner + ..arcToPoint( + Offset(w - depth, radius), + radius: Radius.circular(radius), + ) + // 5. Right Side (Top Half) -> Going OUT to the tip + // We curve slightly to the tip at (w, centerY) + ..lineTo(w - 0.5, centerY - tipRadius) + // 6. Right Tip (Small curve for the point) + ..quadraticBezierTo(w, centerY, w - 0.5, centerY + tipRadius) + // 7. Right Side (Bottom Half) -> Going IN to bottom corner + ..lineTo(w - depth, h - radius) + // 8. Bottom-Right Corner + ..arcToPoint( + Offset(w - depth - radius, h), + radius: Radius.circular(radius), + ) + // 9. Bottom Edge + ..lineTo(radius, h) + // 10. Bottom-Left Corner + ..arcToPoint( + Offset(0, h - radius), + radius: Radius.circular(radius), + ) + // 11. Left Side (Bottom Half) -> Going IN to the indentation + ..lineTo(depth, centerY + tipRadius) + // 12. Left Indent Tip (Small internal curve) + ..quadraticBezierTo(depth - 1.0, centerY, depth, centerY - tipRadius) + // 13. Left Side (Top Half) -> Going OUT to start + ..lineTo(0, radius) + ..close(); + + canvas.drawPath(path, paint); + + final finalBorderColor = borderColor ?? color; + final strokePaint = Paint() + ..color = finalBorderColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 * scale + ..isAntiAlias = true; + canvas.drawPath(path, strokePaint); + } + + @override + bool shouldRepaint(covariant _VerticalChevronPainter oldDelegate) { + return oldDelegate.color != color || + oldDelegate.borderColor != borderColor || + oldDelegate.radius != radius || + oldDelegate.depth != depth || + oldDelegate.scale != scale; + } +} + +class _FlatChevronPainter extends CustomPainter { + _FlatChevronPainter({ + required this.color, + required this.tailWidth, + required this.arrowWidth, + required this.scale, + this.borderColor, + }); + final Color color; + final Color? borderColor; + final double tailWidth; + final double arrowWidth; + final double scale; + + // Original SVG dimensions + static const double _defaultHeight = 90; + static const double _defaultWidth = 234; + static const double _aspectRatio = _defaultWidth / _defaultHeight; + static const Size _defaultSize = Size(_defaultWidth, _defaultHeight); + + // Original feature widths (calculated from SVG coordinates) + // Left tail is approx 15px wide (x=0 to x=15) + static const double _defaultTailWidth = 15; + // Right arrow is approx 38px wide (from flat edge x=196 to tip x=234) + static const double _defaultArrowWidth = 38; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final w = size.width; + final h = size.height; + final s = scale; + + // We calculate the "flat" part start point on the right + final rightFlatX = w - arrowWidth; + + final path = Path() + // --- 1. Top Edge --- + // Start after the tail (x = tailWidth) + ..moveTo(tailWidth, 0) + // Draw straight line to the start of the arrow + ..lineTo(rightFlatX, 0) + // --- 2. Right Arrow Head --- + // All coordinates are relative to `rightFlatX`. + // We use the original SVG control points multiplied by `s` (scale). + // Top curve of arrow + ..cubicTo( + rightFlatX + (4.7 * s), + 0, + rightFlatX + (9.1 * s), + 2.2 * s, + rightFlatX + (12.0 * s), + 6.0 * s, + ) + // Line to tip approach + ..lineTo(rightFlatX + (34.4 * s), 36.0 * s) + // The Tip Rounding + ..cubicTo( + rightFlatX + (38.4 * s), + 41.3 * s, + rightFlatX + (38.4 * s), + 48.6 * s, + rightFlatX + (34.4 * s), + 54.0 * s, + ) + // Line back from tip + ..lineTo(rightFlatX + (12.0 * s), 84.0 * s) + // Bottom curve of arrow + ..cubicTo( + rightFlatX + (9.1 * s), + 87.8 * s, + rightFlatX + (4.7 * s), + 90.0 * s, + rightFlatX, + 90.0 * s, + ) + // --- 3. Bottom Edge --- + ..lineTo(tailWidth, h) + // --- 4. Left Tail (The Wave) --- + // We draw from bottom (h) up to top (0). + // The X coordinates are relative to 0. + // Bottom wave segment + ..cubicTo(2.7 * s, 90.0 * s, -4.4 * s, 76.0 * s, 3.0 * s, 66.0 * s) + // Line Inward + ..lineTo(12.0 * s, 54.0 * s) + // Middle wave segment + ..cubicTo(16.0 * s, 48.6 * s, 16.0 * s, 41.3 * s, 12.0 * s, 36.0 * s) + // Line Outward + ..lineTo(3.0 * s, 24.0 * s) + // Top wave segment + ..cubicTo(-4.4 * s, 14.0 * s, 2.7 * s, 0, tailWidth, 0) + ..close(); + + canvas.drawPath(path, paint); + + final finalBorderColor = borderColor ?? color; + final strokePaint = Paint() + ..color = finalBorderColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 * s + ..isAntiAlias = true; + canvas.drawPath(path, strokePaint); + } + + @override + bool shouldRepaint(covariant _FlatChevronPainter oldDelegate) { + return oldDelegate.color != color || + oldDelegate.borderColor != borderColor || + oldDelegate.scale != scale || + oldDelegate.tailWidth != tailWidth; + } +} + +/// Delegates to a [ChevronButton] that adapts to the current orientation. +class ResponsiveChevronButton extends StatelessWidget { + /// Instantiates a [ResponsiveChevronButton]. + const ResponsiveChevronButton({ + required this.onPressed, + required this.text, + this.color, + this.borderColor, + this.textColor, + this.scale, + this.style, + super.key, + }); + + /// CLick handler. + final VoidCallback? onPressed; + + /// The color of the button. + final Color? color; + + /// Optional border color that defaults to the main color if omitted. + final Color? borderColor; + + /// Optional text color that defaults to the main color if omitted. + final Color? textColor; + + /// Button text. + final String text; + + /// Scalar for the button. + final double? scale; + + /// Button style. + final ChevronButtonType? style; + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, layoutInfo) { + return (style == .vertical) || + (style != .flat && layoutInfo.orientation.isPortrait) + ? ResponsiveSizedBox.builder( + aspectRatioClamp: ( + null, + _VerticalChevronPainter._aspectRatio, + null, + ), + maxSize: _VerticalChevronPainter._defaultSize, + builder: (context, Size size) { + final calculatedScale = + size.height / _VerticalChevronPainter._defaultHeight; + final finalScale = scale != null + ? min(scale!, calculatedScale) + : calculatedScale; + + // 200px width is about the min width before 5-character + // strings start to squish and wrap + final baseFontSize = 44 * (size.width / 200); + + return Center( + child: SizedBox.fromSize( + size: size * (scale ?? 1), + child: ChevronButton.vertical( + builder: (context, double textScale, Color? textColor) { + return WrappedText( + style: (context, theme) { + return theme.typography.h1.copyWith( + // h1 font size is coming as 60, but need + // smaller to fit nicely + fontSize: + baseFontSize * min(textScale, finalScale), + color: textColor, + ); + }, + child: Text(text), + ); + }, + color: color, + borderColor: borderColor, + textColor: textColor, + onPressed: onPressed, + scale: finalScale, + ), + ), + ); + }, + ) + : ResponsiveSizedBox.builder( + aspectRatioClamp: ( + null, + _FlatChevronPainter._aspectRatio, + null, + ), + maxSize: _FlatChevronPainter._defaultSize * (scale ?? 1), + builder: (context, Size size) { + final calculatedScale = + size.height / _FlatChevronPainter._defaultHeight; + final finalScale = scale != null + ? min(scale!, calculatedScale) + : calculatedScale; + return Center( + child: SizedBox.fromSize( + size: size, + child: ChevronButton.flat( + builder: + (context, double textScale, Color? textColor) // + => WrappedText( + style: (context, theme) => + theme.typography.h1.copyWith( + fontSize: + // h1 font size is coming as 60, but + // need about a 10% reduction to size + // this nicely + 54 * min(textScale, finalScale), + color: textColor, + ), + child: Text(text), + ), + color: color, + borderColor: borderColor, + textColor: textColor, + onPressed: onPressed, + scale: finalScale, + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/clipboard_value.dart b/genlatte/genlatte-ui/lib/src/widgets/clipboard_value.dart new file mode 100644 index 0000000..be05311 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/clipboard_value.dart @@ -0,0 +1,80 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:clipboard/clipboard.dart'; +import 'package:logging/logging.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Renders a value which can be copied to the clipboard. +class ClipboardValue extends StatefulWidget { + /// Instantiates a [ClipboardValue]. + const ClipboardValue(this.value, {required this.label, super.key}); + + /// The value to be displayed and copied. + final String value; + + /// Semantic label. + final String label; + + @override + State createState() => _ClipboardValueState(); +} + +class _ClipboardValueState extends State { + @override + Widget build(BuildContext context) => SizedBox( + height: 48, + child: Row( + children: [ + Flexible( + child: Semantics( + label: 'Copy ${widget.label}', + child: Padding( + padding: const EdgeInsets.all(12), + child: GestureDetector( + onTap: () async { + try { + await FlutterClipboard.copy(widget.value); + if (!mounted) return; + showToast( + // ignore: use_build_context_synchronously + context: context, + builder: (context, overlay) { + return SurfaceCard( + child: Basic( + title: Text( + 'Copied ${widget.label}', + ), + ), + ); + }, + ); + } on Exception catch (e, st) { + Logger('ClipboardValue').severe( + 'Failed to copy ${widget.label}} ' + '${widget.value} to ' + 'clipboard', + e, + st, + ); + } + }, + child: const Icon(Icons.copy), + ), + ), + ), + ), + Flexible( + flex: 5, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(widget.value).xSmall.mono, + ], + ), + ), + ], + ), + ); +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/configuration_card.dart b/genlatte/genlatte-ui/lib/src/widgets/configuration_card.dart new file mode 100644 index 0000000..5f406e7 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/configuration_card.dart @@ -0,0 +1,731 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:genlatte/src/screens/app/theme.dart' show AppColors; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' hide Button; +import 'package:text_responsive/text_responsive.dart'; + +/// Cosmetic theme for the card; determines the background color. +enum Flavor { + /// Beige, woody color. + wood, + + /// Light blue-green color. + seaFoam, + + /// I don't even know. I just work here. + weirdGreen; + + /// Background color of the card of this flavor. + Color get backgroundColor => switch (this) { + .wood => const Color(0xFFC9B787), + .seaFoam => const Color(0xFFB7EBE1), + .weirdGreen => const Color(0xFFD1DF90), + }; + + /// Loops through the flavors forever and ever and ever. + static Flavor forIndex(int index) { + return values[index % values.length]; + } +} + +/// Overall shape of the card. +enum CardShape { + /// 160 / 255 + flat, + + /// 335 / 255 + tall, + + /// Uses all available space + expanded; + + /// Height of this type of card under normal conditions. Smaller constraints + /// may end up changing this in practice if the widget is wrapped in a + /// [ResponsiveSizedBox] that provides smaller constraints. + double? get height => switch (this) { + .flat => 160, + .tall => 335, + .expanded => 400, + }; + + /// Width of this type of card under normal conditions. Smaller constraints + /// may end up changing this in practice if the widget is wrapped in a + /// [ResponsiveSizedBox] that provides smaller constraints. + double? get width => switch (this) { + .flat => 255, + .tall => 255, + .expanded => 745, + }; + + /// Aspect ratio of this card under normal conditions. + double get aspectRatio => width! / height!; + + /// Width and height packaged into a [Size] object. + Size get size => Size(width!, height!); +} + +/// A card that presents a latte art configuration question to the user, like, +/// "How crowded is this party?" or "What time of day is it?" +/// +/// If a [CardShape] is provided, then the resulting [ConfigurationCard] will +/// have an exact and predictable size. If no [CardShape] is provided, then the +/// [ConfigurationCard] will expand to fill all available space. +/// +/// The [ConfigurationCard] widget does not play well with unlimited constraints +/// and must be nestled within something that provides limited height and width. +/// +/// [T] is the type of the value returned by [onSelected]. +class ConfigurationCard extends StatelessWidget { + /// Creates a new [ConfigurationCard]. + const ConfigurationCard._({ + required this.flavor, + required this.onSelected, + required this.question, + this.textEditingController, + this.focusNode, + super.key, + }); + + /// Only provided when the question is a [TextQuestion] to maintain focus if + /// the appearance of an on-screen keyboard causes the screen to dramatically + /// alter its layout. + final TextEditingController? textEditingController; + + /// Only provided when the question is a [TextQuestion] to maintain focus if + /// the appearance of an on-screen keyboard causes the screen to dramatically + /// alter its layout. + final FocusNode? focusNode; + + /// + // ignore: strict_raw_type + static ConfigurationCard forQuestion( + Question question, { + required void Function(Object?) onAnswer, + Flavor flavor = Flavor.wood, + TextEditingController? textEditingController, + FocusNode? focusNode, + }) { + switch (question) { + case MultipleChoiceQuestion(): + return ConfigurationCard.buttons( + flavor: flavor, + onSelected: onAnswer, + question: question, + ); + case ZeroToOneQuestion(): + return ConfigurationCard.slider( + flavor: flavor, + onSelected: onAnswer, + question: question, + ); + case NegativeOneToOneQuestion(): + return ConfigurationCard.valueShiftSlider( + flavor: flavor, + onSelected: onAnswer, + question: question, + ); + case TextQuestion(): + return ConfigurationCard.text( + flavor: flavor, + onSelected: onAnswer, + question: question, + textEditingController: textEditingController, + focusNode: focusNode, + ); + } + } + + /// Radio buttons-style controls. + static ConfigurationCard buttons({ + required Flavor flavor, + required void Function(String) onSelected, + required MultipleChoiceQuestion question, + }) { + return ConfigurationCard._( + flavor: flavor, + onSelected: onSelected, + question: question, + ); + } + + /// Slider-style controls. + static ConfigurationCard slider({ + required Flavor flavor, + required void Function(double) onSelected, + required ZeroToOneQuestion question, + }) { + return ConfigurationCard._( + flavor: flavor, + onSelected: onSelected, + question: question, + ); + } + + /// Slider-style controls with "no change" in the middle. + static ConfigurationCard valueShiftSlider({ + required Flavor flavor, + required void Function(double) onSelected, + required NegativeOneToOneQuestion question, + }) { + return ConfigurationCard._( + flavor: flavor, + onSelected: onSelected, + question: question, + ); + } + + /// Text-style controls. + static ConfigurationCard text({ + required Flavor flavor, + required void Function(String) onSelected, + required TextQuestion question, + TextEditingController? textEditingController, + FocusNode? focusNode, + }) { + return ConfigurationCard._( + flavor: flavor, + onSelected: onSelected, + question: question, + textEditingController: textEditingController, + focusNode: focusNode, + ); + } + + /// Determines the background color. + final Flavor flavor; + + /// Complete definition of the question which will help configure this coffee. + final Question question; + + /// Method by which surrounding code learns of user activity. + final void Function(T) onSelected; + + @override + Widget build(BuildContext context) { + return LayoutProvider.builder( + builder: (context, info) { + final edgeInsets = max( + info.constrainingDimension * 0.1, + // Must be an explicit double + // ignore: prefer_int_literals + 16.0, + ).clamp(8.0, 100.0); + + final availableWidth = info.width - (edgeInsets * 2); + final availableHeight = info.height - (edgeInsets * 2); + + // Save 10% padding between + final availableHeightForQuestionOrAnswer = availableHeight * 0.45; + + final double maxFontSize = info.constrainingDimension * 0.15; + final double minFontSize = max(10, info.constrainingDimension * 0.03); + + final h1Style = Theme.of(context).typography.h1; + final textScaler = + MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling; + + (bool, int) questionFits(double fontSize) { + final tp = TextPainter( + text: TextSpan( + text: question.body, + style: h1Style.copyWith(fontSize: fontSize), + ), + textDirection: Directionality.of(context), + textScaler: textScaler, + )..layout(maxWidth: availableWidth); + final lineMetrics = tp.computeLineMetrics(); + final fits = tp.height <= availableHeightForQuestionOrAnswer; + tp.dispose(); + return (fits, lineMetrics.length); + } + + int? maxLinesForQuestion; + + double bestFontSize = minFontSize; + final (fits, maxLines) = questionFits(maxFontSize); + if (fits) { + bestFontSize = maxFontSize; + maxLinesForQuestion = maxLines; + } else { + double low = minFontSize; + double high = maxFontSize; + while (high - low > 0.5) { + final mid = (low + high) / 2; + final (innerFits, innerMaxLines) = questionFits(mid); + if (innerFits) { + bestFontSize = mid; + maxLinesForQuestion = innerMaxLines; + low = mid; + } else { + high = mid; + } + } + } + + final content = Container( + decoration: BoxDecoration( + color: flavor.backgroundColor, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: EdgeInsets.all(edgeInsets), + child: Stack( + children: [ + Positioned( + height: availableHeightForQuestionOrAnswer, + width: availableWidth, + top: 0, + left: 0, + child: ParagraphTextWidget( + question.body, + style: h1Style.copyWith(fontSize: bestFontSize), + maxLines: maxLinesForQuestion, + ), + ), + Positioned( + height: availableHeightForQuestionOrAnswer, + width: availableWidth, + bottom: 0, + left: 0, + child: Align( + alignment: .bottomLeft, + child: switch (question) { + MultipleChoiceQuestion( + :final acceptableAnswers, + :final selectedValue, + ) => + _ConfigurationRadioButtons( + availableSize: Size( + availableWidth, + availableHeightForQuestionOrAnswer, + ), + flavor: flavor, + options: acceptableAnswers, + onSelected: onSelected as void Function(String), + selectedOption: selectedValue, + ), + ZeroToOneQuestion( + :final minValueLabel, + :final maxValueLabel, + :final selectedValue, + ) => + _ConfigurationSlider( + minValueLabel: minValueLabel, + maxValueLabel: maxValueLabel, + selectedValue: selectedValue, + onSelected: onSelected as void Function(double), + sliderType: .absoluteValue, + ), + NegativeOneToOneQuestion( + :final minValueLabel, + :final maxValueLabel, + :final selectedValue, + ) => + _ConfigurationSlider( + minValueLabel: minValueLabel, + maxValueLabel: maxValueLabel, + selectedValue: selectedValue, + onSelected: onSelected as void Function(double), + sliderType: .valueShift, + ), + TextQuestion( + :final answer, + :final helpText, + ) => + _ConfigurationTextInput( + helpText: helpText, + onSelected: onSelected as void Function(String), + selectedValue: answer, + controller: textEditingController, + focusNode: focusNode, + ), + }, + ), + ), + ], + ), + ), + ); + return SizedBox.fromSize(size: info.size, child: content); + }, + ); + } +} + +class _ConfigurationRadioButtons extends StatelessWidget { + const _ConfigurationRadioButtons({ + required this.availableSize, + required this.options, + required this.onSelected, + required this.selectedOption, + required this.flavor, + }); + + final Size availableSize; + final List? options; + + final void Function(String) onSelected; + + final String? selectedOption; + + final Flavor flavor; + + static const double rowSpacing = 8; + static const double columnSpacing = 8; + + @override + Widget build(BuildContext context) { + if (options == null || options!.isEmpty) { + return const SizedBox.shrink(); + } + + final isTight = availableSize.height < 112; + + return DynamicButtonLayout( + availableSize: availableSize, + options: options!, + isTight: isTight, + rowSpacing: rowSpacing, + columnSpacing: columnSpacing, + builder: (context, uiScale) { + return Wrap( + runSpacing: rowSpacing * uiScale, + spacing: columnSpacing * uiScale, + children: + options! // + .map( + (option) => GenLatteConfigurationButton( + onPressed: () { + onSelected(option); + }, + label: option, + isSelected: option == selectedOption, + isTight: isTight, + uiScale: uiScale, + ), + ) + .toList(), + ); + }, + ); + } +} + +/// Determines the type of slider to show. +enum _SliderType { + /// Represents a 0-to-1 value scale. + absoluteValue, + + /// Represents a -1-to-1 value scale, with 0 representing "no change". + valueShift, +} + +class _ConfigurationSlider extends StatelessWidget { + const _ConfigurationSlider({ + required this.sliderType, + required this.minValueLabel, + required this.maxValueLabel, + required this.selectedValue, + required this.onSelected, + }); + + final _SliderType sliderType; + + final String minValueLabel; + + final String maxValueLabel; + + final double? selectedValue; + + final void Function(double) onSelected; + + static const double _sliderHeight = 48; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final isTight = constraints.maxWidth < 250; + final minLabel = isTight + ? minValueLabel.split(' ').first + : minValueLabel; + final maxLabel = isTight + ? maxValueLabel.split(' ').first + : maxValueLabel; + + return WrappedText( + style: (context, theme) => switch (constraints.maxWidth) { + < 100 => theme.typography.large, + < 400 => theme.typography.small, + _ => theme.typography.base, + }, + child: Column( + mainAxisAlignment: .end, + children: [ + Row( + mainAxisAlignment: .spaceBetween, + children: [ + if (sliderType == .valueShift) ...[ + Text(minLabel), + if (!isTight) ...[ + const Text('No change'), + ], + Text(maxLabel, textAlign: .end), + ], + if (sliderType == .absoluteValue) ...[ + Text(minLabel), + Text(maxLabel, textAlign: .end), + ], + ], + ), + const SizedBox(height: 8), + Container( + height: _sliderHeight, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: LayoutBuilder( + builder: (context, constraints) { + return _SliderTrack( + constraints: constraints, + onSelected: onSelected, + sliderType: sliderType, + startingValue: selectedValue, + ); + }, + ), + ), + ], + ), + ); + }, + ); + } +} + +class _SliderTrack extends StatefulWidget { + const _SliderTrack({ + required this.constraints, + required this.sliderType, + required this.onSelected, + required this.startingValue, + }); + + final BoxConstraints constraints; + final _SliderType sliderType; + final void Function(double) onSelected; + final double? startingValue; + + @override + State<_SliderTrack> createState() => _SliderTrackState(); +} + +class _SliderTrackState extends State<_SliderTrack> { + /// Coordinates of the start of the user's drag. A null value here means no + /// drag is underway. + double? _dragStart; + + /// Coordinates of the current position of the user's drag. A null value here + /// means no drag is underway. + double? _dragCurrent; + + /// Computed delta to the starting knob position, stored between -1 and 1. + double _knobPositionDelta = 0; + + /// Absolute value of knob position. + double get _knobPositionAbsolute => + ((widget.constraints.maxWidth / 2) - _halfKnobWidth) * _knobPositionDelta; + + /// Absolute buffer space on either side of the track to prevent the knob from + /// going out of bounds. + double get _halfKnobWidth => widget.constraints.maxHeight / 2; + + /// The maximum number of pixels the center of the knob can travel in either + /// direction. + double get _distanceFromCenterToEdge => + widget.constraints.maxWidth / 2 - _halfKnobWidth; + + @override + void initState() { + super.initState(); + final startingValue = widget.startingValue; + if (startingValue != null) { + _knobPositionDelta = widget.sliderType == .absoluteValue + ? (startingValue * 2) - 1 + : startingValue; + } + } + + @override + void didUpdateWidget(covariant _SliderTrack oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.startingValue != oldWidget.startingValue && + widget.startingValue != null) { + _knobPositionDelta = widget.sliderType == .absoluteValue + ? (widget.startingValue! * 2) - 1 + : widget.startingValue!; + } + } + + void _onDragStart(DragStartDetails details) { + _dragCurrent = details.localPosition.dx; + + if (_knobPositionDelta != 0) { + _dragStart = + details.localPosition.dx - + (_knobPositionDelta * _distanceFromCenterToEdge); + } else { + _dragStart = details.localPosition.dx; + } + } + + void _onDragUpdate(DragUpdateDetails details) { + setState(() { + _dragCurrent = details.localPosition.dx; + + _knobPositionDelta = + ((_dragCurrent! - _dragStart!) / _distanceFromCenterToEdge) // + .clamp(-1, 1); + }); + } + + void _onDragEnd(DragEndDetails details) { + // _knobPositionDelta is a float between -1 and 1, which means it is already + // in the correct range for a [.valueShift] slider; but must be normalized + // to 0-1 for [.absoluteValue] sliders. To do this, we add 1, shifting the + // range from 0 to 2, then divide by 2 to make it 0 to 1. + final normalized = widget.sliderType == .absoluteValue + ? (_knobPositionDelta + 1) / 2 + : _knobPositionDelta; + widget.onSelected(normalized); + setState(() { + _dragStart = null; + _dragCurrent = null; + }); + } + + /// Where to place the left side of the slider such that its center aligns + /// with the center of the track. + double get _startingKnobPosition => + (widget.constraints.maxWidth / 2) - (widget.constraints.maxHeight / 2); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Grey line acting as the track for the slider + Positioned( + left: _halfKnobWidth, + width: widget.constraints.maxWidth - (_halfKnobWidth * 2), + top: widget.constraints.maxHeight * 0.5, + height: 1, + child: Container( + decoration: const BoxDecoration( + color: Color(0xFFA2A2A2), + ), + ), + ), + // Blue knob + Positioned( + left: _startingKnobPosition + _knobPositionAbsolute, + width: widget.constraints.maxHeight, + top: 0, + // This measurement creates a rounded corner square + height: widget.constraints.maxHeight, + child: GestureDetector( + onHorizontalDragStart: _onDragStart, + onHorizontalDragUpdate: _onDragUpdate, + onHorizontalDragEnd: _onDragEnd, + child: Container( + decoration: BoxDecoration( + color: AppColors.googleBlue, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ], + ); + } +} + +class _ConfigurationTextInput extends StatefulWidget { + const _ConfigurationTextInput({ + required this.helpText, + required this.onSelected, + required this.selectedValue, + this.controller, + this.focusNode, + }); + + final String helpText; + final void Function(String) onSelected; + final String? selectedValue; + final TextEditingController? controller; + final FocusNode? focusNode; + + @override + State<_ConfigurationTextInput> createState() => + _ConfigurationTextInputState(); +} + +class _ConfigurationTextInputState extends State<_ConfigurationTextInput> { + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = + widget.controller ?? TextEditingController(text: widget.selectedValue); + } + + @override + void dispose() { + if (widget.controller == null) { + _controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + // If the card is too short, then larger text sizes cause the input + // to overflow. + final double floor = constraints.maxHeight > 40 ? 0.9 : 0.5; + final double inputScalar = max(floor, constraints.maxWidth / 500); + + final textStyle = Theme.of(context).typography.large.copyWith( + fontSize: Theme.of(context).typography.large.fontSize! * inputScalar, + ); + return TextField( + decoration: BoxDecoration( + border: const Border(), // defaults to BorderStyle.none + borderRadius: BorderRadius.all( + Radius.circular(8 * inputScalar), + ), + color: Theme.of(context).colorScheme.input, + ), + controller: _controller, + focusNode: widget.focusNode, + onChanged: widget.onSelected, + enableInteractiveSelection: false, + placeholder: Text(widget.helpText, style: textStyle), + padding: EdgeInsets.symmetric( + horizontal: 16 * inputScalar, + vertical: 12 * inputScalar, + ), + style: textStyle, + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/design_scalar.dart b/genlatte/genlatte-ui/lib/src/widgets/design_scalar.dart new file mode 100644 index 0000000..1a2a737 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/design_scalar.dart @@ -0,0 +1,76 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// /// @docImport 'aspect_ratio_scalar.dart'; +// library; + +// import 'dart:math' as math; +// import 'package:flutter/widgets.dart'; + +// /// A widget that provides a [DesignScalar] widget to its descendants. +// class ResponsiveDesignRoot extends StatelessWidget { +// /// Instantiates a new [ResponsiveDesignRoot]. +// const ResponsiveDesignRoot({ +// required this.designSize, +// required this.child, +// super.key, +// }); + +// /// Size of the original design, probably a tidy value like 1920x1080. +// final Size designSize; + +// /// The widget to render. +// final Widget child; + +// @override +// Widget build(BuildContext context) { +// // Get the total available screen size +// final screenSize = MediaQuery.sizeOf(context); + +// // Calculate the scale factor (using 'contain' logic to prevent clipping) +// final widthScale = screenSize.width / designSize.width; +// final heightScale = screenSize.height / designSize.height; +// final scaleFactor = math.min(widthScale, heightScale); + +// // Provide the calculated scale factor to the rest of the tree +// return DesignScalar( +// scaleFactor: scaleFactor, +// child: child, +// ); +// } +// } + +// /// Tells descendant widgets how much to scale design system values by. +// /// This is useful for global rules like padding between elements. To +// /// responsively scale the internals of a complicated widget, use a +// /// [AspectRatioScalar]. +// class DesignScalar extends InheritedWidget { +// /// Instantiates a new [DesignScalar]. +// const DesignScalar({ +// required this.scaleFactor, +// required super.child, +// super.key, +// }); + +// /// The lesser of two ratios (height and width) between the design system's +// /// dimensions and the available space. +// final double scaleFactor; + +// /// Allows descendant widgets to easily grab the scale factor. +// static double of(BuildContext context) { +// final DesignScalar? result = context +// .dependOnInheritedWidgetOfExactType(); +// assert( +// result != null, +// 'No DesignScalar found in context. ' +// 'Ensure you wrapped your app/screen in a DesignScalarRoot.', +// ); +// return result!.scaleFactor; +// } + +// @override +// bool updateShouldNotify(DesignScalar oldWidget) { +// return scaleFactor != oldWidget.scaleFactor; +// } +// } diff --git a/genlatte/genlatte-ui/lib/src/widgets/dynamic_button_layout.dart b/genlatte/genlatte-ui/lib/src/widgets/dynamic_button_layout.dart new file mode 100644 index 0000000..a196d74 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/dynamic_button_layout.dart @@ -0,0 +1,182 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/widgets.dart'; +import 'package:genlatte/src/widgets/buttons.dart'; + +/// Builder function that receives the calculated scaling factor for the UI. +typedef DynamicButtonLayoutBuilder = + Widget Function( + BuildContext context, + double uiScale, + ); + +/// A widget that analytically figures out the optimal UI scaling factor +/// necessary to fit a sequence of GenLatteConfigurationButton widgets into +/// the provided horizontal and vertical space. +class DynamicButtonLayout extends StatefulWidget { + /// Creates a new DynamicButtonLayout. + const DynamicButtonLayout({ + required this.availableSize, + required this.options, + required this.isTight, + required this.builder, + required this.rowSpacing, + required this.columnSpacing, + this.minScale = 0.1, + this.maxScale = 2.0, + super.key, + }); + + /// The size the layout must fit within. + final Size availableSize; + + /// The labels that will be placed in the buttons. + final List options; + + /// Whether the buttons should be rendered with tight padding. + final bool isTight; + + /// Render the final layout with the calculated `uiScale`. + final DynamicButtonLayoutBuilder builder; + + /// Vertical spacing between wrapped rows. + final double rowSpacing; + + /// Horizontal spacing between buttons on the same row. + final double columnSpacing; + + /// The minimum scale factor to softly bottom out at. + final double minScale; + + /// The maximum scale factor to search up to. + final double maxScale; + + @override + State createState() => _DynamicButtonLayoutState(); +} + +class _DynamicButtonLayoutState extends State { + // Cache to store the computed optimal UI scale for a given set of parameters. + static final Map _scaleCache = {}; + + @override + Widget build(BuildContext context) { + if (widget.options.isEmpty) { + return widget.builder(context, 1); + } + + final defaultStyle = DefaultTextStyle.of(context).style; + final textScaler = + MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling; + + // Generate a hash representing all relevant layout inputs. + final hashKey = Object.hash( + widget.availableSize, + Object.hashAll(widget.options), + widget.isTight, + widget.rowSpacing, + widget.columnSpacing, + widget.minScale, + widget.maxScale, + defaultStyle, + textScaler, + ); + + if (_scaleCache.containsKey(hashKey)) { + return widget.builder(context, _scaleCache[hashKey]!); + } + + double low = widget.minScale; + double high = widget.maxScale; + double bestScale = low; + + // Binary search to find the highest scale that fits. + if (_fits(high, defaultStyle, textScaler)) { + bestScale = high; + } else { + while (high - low > 0.01) { + final mid = (low + high) / 2; + if (_fits(mid, defaultStyle, textScaler)) { + bestScale = mid; + low = mid; + } else { + high = mid; + } + } + } + + _scaleCache[hashKey] = bestScale; + return widget.builder(context, bestScale); + } + + bool _fits( + double scale, + TextStyle baseStyle, + TextScaler textScaler, + ) { + final paddingX = + (widget.isTight + ? GenLatteConfigurationButton.tightPadding.horizontal + : GenLatteConfigurationButton.normalPadding.horizontal) * + scale; + final paddingY = + (widget.isTight + ? GenLatteConfigurationButton.tightPadding.vertical + : GenLatteConfigurationButton.normalPadding.vertical) * + scale; + + // We add an extra 4 pixels conservatively to account for shadcn + // internal border/focus ring spacing that might be unaccounted for. + const borderSlopX = 6.0; + const borderSlopY = 6.0; + + double totalHeight = 0; + double currentRowWidth = 0; + double rowHeight = 0; + + for (int i = 0; i < widget.options.length; i++) { + // Calculate exactly for this scale using proper text scaler and style + final tp = TextPainter( + text: TextSpan( + text: widget.options[i], + style: baseStyle.copyWith(fontSize: 14 * scale), + ), + textDirection: TextDirection.ltr, + textScaler: textScaler, + )..layout(); + + // We conservatively buffer the size by 1.05 and add the slop + final buttonWidth = tp.width * 1.05 + paddingX + borderSlopX; + final buttonHeight = tp.height * 1.05 + paddingY + borderSlopY; + + if (currentRowWidth + buttonWidth > widget.availableSize.width) { + // Need to wrap to the next row (if we already have items) + if (currentRowWidth > 0) { + totalHeight += rowHeight + (widget.rowSpacing * scale); + } + currentRowWidth = buttonWidth + (widget.columnSpacing * scale); + rowHeight = buttonHeight; + + if (buttonWidth > widget.availableSize.width) { + // Even alone, this button is too wide to fit the width constraint! + return false; + } + } else { + // Fits perfectly on the current row + currentRowWidth += buttonWidth + (widget.columnSpacing * scale); + rowHeight = max(rowHeight, buttonHeight); + } + } + + // Add the height of the last row + if (rowHeight > 0) { + totalHeight += rowHeight; + } + + return totalHeight <= widget.availableSize.height; + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/extensions.dart b/genlatte/genlatte-ui/lib/src/widgets/extensions.dart new file mode 100644 index 0000000..9522543 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/extensions.dart @@ -0,0 +1,30 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +/// +extension DebuggableWidget on Widget { + /// + Widget border({Color? color}) { + return Container( + decoration: BoxDecoration( + border: Border.all( + color: + color ?? + const Color.from( + alpha: 1, + red: 1, + green: 0, + blue: 0, + ), + ), + ), + child: this, + ); + } + + /// + Widget withBorder({Color? color}) => border(color: color); +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/footer.dart b/genlatte/genlatte-ui/lib/src/widgets/footer.dart new file mode 100644 index 0000000..910f028 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/footer.dart @@ -0,0 +1,92 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:genlatte/src/widgets/responsive_sized_box.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// The 3-logo footer widget. +class Footer extends StatelessWidget { + /// Instantiates the footer widget. + const Footer({this.fontColor = AppColors.white, this.uiScale = 1.0}) + : super(key: const ValueKey('app-footer')); + + /// The color of the footer text. + final Color fontColor; + + /// The scale of the footer. + final double uiScale; + + /// Default size of all assets is about ~301 pixels. This size was derived + /// by first laying out the assets and measuring; not by picking a value and + /// squeezing the assets to fit. + static const double defaultWidth = 302; + + /// Default height of all assets is about ~24 pixels. This size was derived + /// by first laying out the assets and measuring; not by picking a value and + /// squeezing the assets to fit. + static const double defaultHeight = 24; + + static const double _defaultFontSize = 12; + + static const double _defaultLogoWidth = 32; + static const double _defaultSpacerWidth = 4; + + @override + Widget build(BuildContext context) { + return ResponsiveSizedBox.builder( + aspectRatioClamp: (null, 302 / 24, null), + maxSize: Size(defaultWidth * uiScale, defaultHeight * uiScale), + builder: (context, size) { + final heightScale = size.height / defaultHeight; + final widthScale = size.width / defaultWidth; + final double scale = min(heightScale, widthScale); + final double spacerWidth = _defaultSpacerWidth * widthScale; + final double fontSize = _defaultFontSize * scale; + final double logoWidth = _defaultLogoWidth * scale; + + // Create repeatable widgets + final spacer = SizedBox(width: spacerWidth); + final verticalBar = _VerticalBar(scale: scale); + final style = TextStyle(color: fontColor, fontSize: fontSize); + return Row( + children: [ + Image.asset('assets/gemini-logo.png', width: logoWidth), + spacer, + Text('Gemini', style: style), + verticalBar, + Image.asset('assets/firebase-logo.png', width: logoWidth), + spacer, + Text('Firebase', style: style), + verticalBar, + Image.asset('assets/flutter-logo.png', width: logoWidth), + spacer, + Text('Flutter', style: style), + ], + ); + }, + ); + } +} + +class _VerticalBar extends StatelessWidget { + const _VerticalBar({required this.scale}); + + final double scale; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12 * scale), + child: VerticalDivider( + width: 1 * scale, + thickness: 1 * scale, + color: const Color(0xFF838383), + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/genlatte_scaffold.dart.dart b/genlatte/genlatte-ui/lib/src/widgets/genlatte_scaffold.dart.dart new file mode 100644 index 0000000..bdba06c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/genlatte_scaffold.dart.dart @@ -0,0 +1,129 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:genlatte/src/screens/app/app.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Wraps a shadcn [Scaffold] to supply the dotted background behind all +/// customer-facing screens. +class GenLatteScaffold extends StatelessWidget { + /// Instantiates a [GenLatteScaffold] widget. + const GenLatteScaffold({ + required this.child, + this.headers = const [], + this.footers = const [], + super.key, + }); + + /// Header widgets passed straight to the shadcn [Scaffold]. + final List headers; + + /// Footer widgets passed straight to the shadcn [Scaffold]. + final List footers; + + /// The UI to sit atop the dotted background. + final Widget child; + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: AppColors.almostBlack, + child: Stack( + fit: StackFit.expand, + children: [ + // 1. The Background Layer + LayoutBuilder( + builder: (context, constraints) { + return CustomPaint( + size: Size(constraints.maxWidth, constraints.maxHeight), + painter: _CircleTessellationPainter( + circleColor: AppColors.black, + ), + ); + }, + ), + // 2. The Content Layer + Scaffold( + backgroundColor: Colors.transparent, + headers: headers, + footers: footers, + child: child, + ), + ], + ), + ); + } +} + +class _CircleTessellationPainter extends CustomPainter { + _CircleTessellationPainter({required this.circleColor}); + final Color circleColor; + + /// Constants from requirements + static const double circleRadius = 100; + static const double circleDiameter = circleRadius * 2; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = circleColor + ..style = PaintingStyle.fill; + + final double centerX = size.width / 2; + final double centerY = size.height / 2; + + // Calculate how many columns/rows we need to cover the screen plus buffers + // We add 1 to the count to ensure circles clipped by the edge are still + // drawn. + final int horizontalCount = (centerX / circleDiameter).ceil() + 1; + final int verticalCount = (centerY / circleDiameter).ceil() + 1; + + // Helper function to draw a vertical column of circles at a specific X + void drawColumn(double xCenter) { + // Draw center circle + canvas.drawCircle(Offset(xCenter, centerY), circleRadius, paint); + + // Draw circles moving Up and Down from center + for (int i = 1; i <= verticalCount; i++) { + final offset = i * circleDiameter; + canvas + ..drawCircle( + Offset(xCenter, centerY - offset), + circleRadius, + paint, + ) + ..drawCircle( + Offset(xCenter, centerY + offset), + circleRadius, + paint, + ); + } + } + + // --- Draw Columns --- + + // The requirement states the vertical center is a junction of two columns. + // Innermost centers are 180px left and right of center. + + // Draw the Right side columns (starting at center + 180) + for (int i = 0; i <= horizontalCount; i++) { + // center + 180, center + 540, etc. + final x = centerX + circleRadius + (i * circleDiameter); + drawColumn(x); + } + + // Draw the Left side columns (starting at center - 180) + for (int i = 0; i <= horizontalCount; i++) { + // center - 180, center - 540, etc. + final x = centerX - circleRadius - (i * circleDiameter); + drawColumn(x); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + // Since the circles are static, we never need to repaint unless rebuilt + return false; + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/latte_image.dart b/genlatte/genlatte-ui/lib/src/widgets/latte_image.dart new file mode 100644 index 0000000..e38331a --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/latte_image.dart @@ -0,0 +1,75 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:genlatte/src/widgets/stacked_images.dart'; + +/// Renders a latte image with the cup outline. +class LatteImageWidget extends StatelessWidget { + /// Creates a [LatteImageWidget] widget. + const LatteImageWidget({ + required this.imageUrl, + this.dimension, + this.topScaleRatio = 0.92, + this.thumbnailSize = false, + super.key, + }); + + /// Size of the widget. + final double? dimension; + + /// The latte image to show above the coffee cup image. + final String imageUrl; + + /// The scale ratio of the [imageUrl] image relative to the cup background. + final double topScaleRatio; + + /// Set this to `true` to convey that the widget will always be + /// thumbnail-sized and thus can use a low-resolution asset. + /// + /// Defaults to `false`. + final bool thumbnailSize; + + @override + Widget build(BuildContext context) { + final child = StackedImages( + bottom: Image.asset( + thumbnailSize + ? 'assets/latte-background-thumb.png' + : 'assets/latte-background.png', + ), + top: _MultiplyBlend( + child: ClipOval(child: Image.network(imageUrl, fit: .cover)), + ), + topScaleRatio: topScaleRatio, + ); + + if (dimension != null) { + return SizedBox.square(dimension: dimension, child: child); + } + return child; + } +} + +class _MultiplyBlend extends SingleChildRenderObjectWidget { + const _MultiplyBlend({super.child}); + + @override + RenderObject createRenderObject(BuildContext context) => _RenderMultiply(); +} + +class _RenderMultiply extends RenderProxyBox { + @override + void paint(PaintingContext context, Offset offset) { + if (child != null) { + context.canvas.saveLayer( + offset & size, + Paint()..blendMode = BlendMode.multiply, + ); + context.paintChild(child!, offset); + context.canvas.restore(); + } + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/layout_provider.dart b/genlatte/genlatte-ui/lib/src/widgets/layout_provider.dart new file mode 100644 index 0000000..af6104c --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/layout_provider.dart @@ -0,0 +1,58 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:genlatte/src/widgets/responsive_sized_box.dart'; +import 'package:provider/provider.dart'; + +/// A widget that provides [LayoutInformation] to its descendants. +class LayoutProvider extends StatelessWidget { + /// Child pattern for [LayoutProvider]. + factory LayoutProvider({ + required Widget child, + Key? key, + }) => LayoutProvider._( + key: key, + child: child, + ); + + /// Creates a new [LayoutProvider]. + const LayoutProvider._({ + this.builder, + this.child, + super.key, + }) : assert( + child != null || builder != null, + 'Provide either child or builder', + ); + + /// Builder pattern for [LayoutProvider]. + factory LayoutProvider.builder({ + required Widget Function(BuildContext, LayoutInformation) builder, + Key? key, + }) => LayoutProvider._( + key: key, + builder: builder, + ); + + /// The child widget. + final Widget? child; + + /// Builder function that receives the optimal size and returns the widget to + /// display. Provide this or [child]. + final Widget Function(BuildContext, LayoutInformation)? builder; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final info = LayoutInformation.fromBoxConstraints(constraints); + return Provider.value( + value: info, + child: child ?? builder!(context, info), + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/loading_dash.dart b/genlatte/genlatte-ui/lib/src/widgets/loading_dash.dart new file mode 100644 index 0000000..7e683d9 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/loading_dash.dart @@ -0,0 +1,102 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart' + show TargetPlatform, defaultTargetPlatform, kIsWeb; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:video_player/video_player.dart'; +import 'package:web_browser_detect/web_browser_detect.dart'; + +/// Displays the loading animation of Dash imagining various Google logos as +/// latte art. +/// +/// Renders nothing on Safari web, which does not support transparency in +/// videos. +class LoadingDash extends StatelessWidget { + /// Instantiates a [LoadingDash]. + const LoadingDash({super.key}); + + @override + Widget build(BuildContext context) { + if (kIsWeb) { + final browser = Browser(); + if (browser.browserAgent == BrowserAgent.safari) { + return const SizedBox.shrink(); + } + } + return const _LoadingDashInner(); + } +} + +class _LoadingDashInner extends StatefulWidget { + const _LoadingDashInner(); + + @override + State<_LoadingDashInner> createState() => _LoadingDashState(); +} + +class _LoadingDashState extends State<_LoadingDashInner> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + + // Use the asset constructor + _controller = + VideoPlayerController.asset( + kIsWeb + ? 'assets/videos/Dash_Loading_1080px.webm' + : 'assets/videos/Dash_Loading_1080px_H.265.mov', + ) + ..initialize() + .then((_) { + if (!mounted) return; + _controller.setLooping(true).ignore(); + _controller.play().ignore(); + // Ensure the first frame is shown after the video is + // initialized. + setState(() {}); + }) + .onError((error, stackTrace) { + debugPrint('Failed to initialize video player: $error'); + }) + .ignore(); + } + + @override + Widget build(BuildContext context) { + Widget widget = VideoPlayer(_controller); + + // Android isn't honoring transparency in either webm or h265, but this + // color filter drops the alpha channel to 0 for black pixels, which is the + // same for our purposes. + final isAndroid = defaultTargetPlatform == TargetPlatform.android; + + if (isAndroid) { + widget = ColorFiltered( + colorFilter: const ColorFilter.matrix([ + 1, 0, 0, 0, 0, // + 0, 1, 0, 0, 0, // + 0, 0, 1, 0, 0, // + 1, 1, 1, 0, -1, // + ]), + child: widget, + ); + } + + return Center( + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: widget, + ), + ); + } + + @override + void dispose() { + _controller.dispose().ignore(); + super.dispose(); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/machine_selection.dart b/genlatte/genlatte-ui/lib/src/widgets/machine_selection.dart new file mode 100644 index 0000000..b8080a4 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/machine_selection.dart @@ -0,0 +1,190 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; + +/// A reusable widget that allows users to select a [Machine] from a list. +class MachineSelection extends StatefulWidget { + /// Creates a new [MachineSelection] widget. + const MachineSelection({ + required this.machines, + required this.onSubmitted, + this.initialSelection, + super.key, + }); + + /// The list of available machines to choose from. + final List machines; + + /// The machine that should be selected by default. + final Machine? initialSelection; + + /// Callback fired when the user selects a machine and clicks "Submit". + final void Function(Machine) onSubmitted; + + @override + State createState() => _MachineSelectionState(); +} + +class _MachineSelectionState extends State { + Machine? _selectedMachine; + + @override + void initState() { + super.initState(); + _selectedMachine = widget.initialSelection; + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Flexible( + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget.machines.length, + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final machine = widget.machines[index]; + final isSelected = _selectedMachine?.id == machine.id; + + return _MachineSelectionTile( + machine: machine, + isSelected: isSelected, + onTap: () => setState(() => _selectedMachine = machine), + ); + }, + ), + ), + const SizedBox(height: 24), + if (_selectedMachine != null) + Flexible( + child: GenLatteOutlinedButton.dark( + label: 'Submit Selection', + onPressed: () => widget.onSubmitted(_selectedMachine!), + ), + ), + ], + ); + } +} + +class _MachineSelectionTile extends StatefulWidget { + const _MachineSelectionTile({ + required this.machine, + required this.isSelected, + required this.onTap, + }); + + final Machine machine; + final bool isSelected; + final VoidCallback onTap; + + @override + State<_MachineSelectionTile> createState() => _MachineSelectionTileState(); +} + +class _MachineSelectionTileState extends State<_MachineSelectionTile> { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + final isSelected = widget.isSelected; + + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: widget.onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF18181B) // Zinc 900 + : (_isHovered + ? const Color(0xFFF4F4F5) + : Colors.white), // Zinc 100 + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? const Color(0xFF18181B) + : const Color(0xFFE4E4E7), // Zinc 200 + width: 2, + ), + boxShadow: [ + if (isSelected || _isHovered) + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF27272A) // Zinc 800 + : const Color(0xFFF4F4F5), // Zinc 100 + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.coffee_maker_outlined, + color: isSelected + ? Colors.white + : const Color(0xFF71717A), // Zinc 500 + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.machine.name, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : const Color(0xFF18181B), + ), + ), + const SizedBox(height: 2), + Text( + widget.machine.isBlackAndWhite + ? 'Black & White 🔲' + : 'Full Color 🎨', + style: TextStyle( + fontSize: 14, + color: isSelected + ? Colors.white.withValues(alpha: 0.7) + : const Color(0xFF71717A), + ), + ), + ], + ), + ), + if (isSelected) + const Icon( + Icons.check_circle, + color: Colors.white, + ), + ], + ), + ), + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/overflow_marquee.dart b/genlatte/genlatte-ui/lib/src/widgets/overflow_marquee.dart new file mode 100644 index 0000000..e1ff64f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/overflow_marquee.dart @@ -0,0 +1,162 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// A widget that takes a string and, if it overflows its horizontal +/// constraints, smoothly scrolls it in place to reveal the hidden text. +class OverflowMarquee extends StatefulWidget { + /// Creates an [OverflowMarquee]. + const OverflowMarquee({ + required this.text, + super.key, + this.style, + this.scrollVelocity = 35.0, + this.pauseDuration = const Duration(seconds: 5), + this.loop = false, + }); + + /// The text to display. + final String text; + + /// The style of the text. + final TextStyle? style; + + /// The scrolling velocity in logical pixels per second. + final double scrollVelocity; + + /// The pause duration at the beginning and the end of the scroll. + final Duration pauseDuration; + + /// Whether to instantly jump to the start after reaching the end (true), + /// or to scroll back to the start (false). Default is false (ping-pong). + final bool loop; + + @override + State createState() => _OverflowMarqueeState(); +} + +class _OverflowMarqueeState extends State { + late ScrollController _scrollController; + bool _isAnimating = false; + int _runToken = 0; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + WidgetsBinding.instance.addPostFrameCallback( + (_) => unawaited(_checkAndStart()), + ); + } + + @override + void didUpdateWidget(OverflowMarquee oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.text != oldWidget.text || widget.style != oldWidget.style) { + unawaited(_checkAndStart()); + } + } + + @override + void dispose() { + _runToken++; + _scrollController.dispose(); + super.dispose(); + } + + Future _checkAndStart() async { + final currentToken = ++_runToken; + if (!_scrollController.hasClients) return; + + // Reset position to 0 if the text or constraints updated. + _scrollController.jumpTo(0); + + // Give the layout a frame to calculate the new maxScrollExtent. + await Future.delayed(Duration.zero); + + if (!mounted || + _runToken != currentToken || + !_scrollController.hasClients) { + return; + } + + final maxExtent = _scrollController.position.maxScrollExtent; + if (maxExtent > 0) { + unawaited(_runLoop(currentToken)); + } + } + + Future _runLoop(int token) async { + if (_isAnimating) return; + _isAnimating = true; + + try { + while (mounted && _runToken == token && _scrollController.hasClients) { + // Pause at the start + await Future.delayed(widget.pauseDuration); + if (!mounted || _runToken != token || !_scrollController.hasClients) { + break; + } + + final maxExtent = _scrollController.position.maxScrollExtent; + if (maxExtent <= 0) break; // Stop if text no longer overflows + + // Animate to the end + final duration = Duration( + milliseconds: (maxExtent / widget.scrollVelocity * 1000).toInt(), + ); + + await _scrollController.animateTo( + maxExtent, + duration: duration, + curve: Curves.linear, + ); + + if (!mounted || _runToken != token || !_scrollController.hasClients) { + break; + } + + // Pause at the end + await Future.delayed(widget.pauseDuration); + if (!mounted || _runToken != token || !_scrollController.hasClients) { + break; + } + + // Return to start + if (widget.loop) { + _scrollController.jumpTo(0); + } else { + await _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } + } + } finally { + if (_runToken == token) { + _isAnimating = false; + } + } + } + + @override + Widget build(BuildContext context) { + // SingleChildScrollView automatically respects the Directionality of the + // context, scrolling properly based on text direction (LTR vs RTL). + return SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + child: Text( + widget.text, + style: widget.style, + maxLines: 1, + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/responsive_sized_box.dart b/genlatte/genlatte-ui/lib/src/widgets/responsive_sized_box.dart new file mode 100644 index 0000000..202127a --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/responsive_sized_box.dart @@ -0,0 +1,374 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/widgets.dart'; + +/// Responsive layout information about the available space. Primarily derived +/// from [BoxConstraints]. +class LayoutInformation { + /// Instantiates a [LayoutInformation]. + const LayoutInformation._(this.constraints); + + /// Builds a [LayoutInformation] from the given [BoxConstraints]. + factory LayoutInformation.fromBoxConstraints(BoxConstraints constraints) => + LayoutInformation._(constraints); + + /// The constraints of the available space. + final BoxConstraints constraints; + + /// Horizontal pixels of the available space. + double get width => constraints.maxWidth; + + /// Vertical pixels of the available space. + double get height => constraints.maxHeight; + + /// The smaller of the width and height. + double get constrainingDimension => min(width, height); + + /// The larger of the width and height. + double get unconstrainingDimension => max(width, height); + + /// The size of the available space. + Size get size => Size(width, height); + + /// The aspect ratio of the available space. + double get aspectRatio => width / height; + + /// Returns the most constraining scale for a given ideal size. + double constrainedScale(Size idealSize) => min( + width / idealSize.width, + height / idealSize.height, + ); + + /// Returns the least constraining scale for a given ideal size. + double unconstrainedScale(Size idealSize) => max( + width / idealSize.width, + height / idealSize.height, + ); + + /// Aspect ratio category of this layout. + LayoutOrientation get orientation => width == height + ? .square + : width > height + ? .landscape + : .portrait; +} + +/// The orientation of a Cartesian object. +enum LayoutOrientation { + /// Taller than wide. + portrait(), + + /// Wider than tall. + landscape, + + /// Same height and width. + square; + + /// True if this orientation is landscape. + bool get isLandscape => this == .landscape; + + /// True if this orientation is portrait. + bool get isPortrait => this == .portrait; + + /// True if this orientation is square. + bool get isSquare => this == .square; + + /// Returns the desired Flex [Axis] for this orientation. e.g., if we are in a + /// landscape orientation, we want to lay out our children horizontally. + Axis get axis => this == .landscape ? .horizontal : .vertical; +} + +/// {@template AcceptableAspectRatios} +/// Range of allowed aspect ratios. +/// +/// $1 is optional and represents the lowest (tallest) acceptable aspect ratio. +/// $2 is the ideal aspect ratio which will be selected if possible. +/// $3 is optional and represents the highest (widest) acceptable aspect ratio. +/// {@endtemplate} +typedef AcceptableAspectRatios = (double?, double?, double?); + +/// A widget that sizes itself based on the available space and the +/// range of acceptable aspect ratios. +class ResponsiveSizedBox extends StatelessWidget { + /// Instantiates a [ResponsiveSizedBox]. + factory ResponsiveSizedBox({ + required AcceptableAspectRatios aspectRatioClamp, + required Widget child, + Alignment? alignment = .center, + Size? maxSize, + Key? key, + }) => ResponsiveSizedBox._( + alignment: alignment, + aspectRatioClamp: aspectRatioClamp, + maxSize: maxSize, + key: key, + child: child, + ); + + /// Builder pattern for [ResponsiveSizedBox]. + factory ResponsiveSizedBox.builder({ + required AcceptableAspectRatios aspectRatioClamp, + required Widget Function(BuildContext, Size) builder, + Alignment? alignment = .center, + Size? maxSize, + Key? key, + }) => ResponsiveSizedBox._( + alignment: alignment, + aspectRatioClamp: aspectRatioClamp, + builder: builder, + maxSize: maxSize, + key: key, + ); + + /// Generative constructor for [ResponsiveSizedBox]. + const ResponsiveSizedBox._({ + required this.aspectRatioClamp, + required this.alignment, + this.builder, + this.maxSize, + this.child, + super.key, + }) : assert( + child != null || builder != null, + 'Provide either child or builder', + ); + + /// {@macro AcceptableAspectRatios} + final AcceptableAspectRatios aspectRatioClamp; + + /// The alignment of the child within the available space. + final Alignment? alignment; + + /// Optional maximum size for the widget. + final Size? maxSize; + + /// Descendant widget to scale responsively. Provide this or [builder]. + final Widget? child; + + /// Builder function that receives the optimal size and returns the widget to + /// display. Provide this or [child]. + final Widget Function(BuildContext, Size)? builder; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final scalar = AspectRatioScalar( + aspectRatioClamp, + constraints: constraints, + maxSize: maxSize, + ); + + Widget result = child ?? builder!(context, scalar.optimalSize); + + result = SizedBox.fromSize( + size: scalar.optimalSize, + child: result, + ); + + if (alignment != null) { + result = Align( + alignment: alignment!, + child: result, + ); + } + return result; + }, + ); + } +} + +/// Provides sizing information based on available dimensions and aspect ratios. +/// +/// Usage: +/// ```dart +/// +/// // Tallest allowed +/// double minAcceptableAspectRatio = 0.5; +/// // Ideal +/// double idealAspectRatio = 0.75; +/// // Widest allowed +/// double maxAcceptableAspectRatio = 1; +/// +/// final scalar = AspectRatioScalar( +/// (minAcceptableAspectRatio, idealAspectRatio, maxAcceptableAspectRatio), +/// constraints: constraints, +/// maxSize: const Size(defaultWidth, defaultHeight), +/// ); +/// +/// final scaledSize = scalar.optimalSize; +/// final scaledHeight = scalar.scaleHeight(100); +/// final scaledWidth = scalar.scaleWidth(100); +/// ``` +class AspectRatioScalar { + /// Instantiates an [AspectRatioScalar]. + AspectRatioScalar( + this.aspectRatioClamp, { + required this.constraints, + this.maxSize, + }) : assert(() { + if (aspectRatioClamp.$1 != null && + aspectRatioClamp.$2 != null && + aspectRatioClamp.$1! >= aspectRatioClamp.$2!) { + throw AssertionError( + 'aspectRatioClamp min must be less than ideal. ' + '${aspectRatioClamp.$1} >= ${aspectRatioClamp.$2}', + ); + } + if (aspectRatioClamp.$1 != null && + aspectRatioClamp.$3 != null && + aspectRatioClamp.$1! >= aspectRatioClamp.$3!) { + throw AssertionError( + 'aspectRatioClamp min must be less than max. ' + '${aspectRatioClamp.$1} >= ${aspectRatioClamp.$3}', + ); + } + if (aspectRatioClamp.$2 != null && + aspectRatioClamp.$3 != null && + aspectRatioClamp.$2! >= aspectRatioClamp.$3!) { + throw AssertionError( + 'aspectRatioClamp ideal must be less than max. ' + '${aspectRatioClamp.$2} >= ${aspectRatioClamp.$3}', + ); + } + return true; + }(), 'Invalid aspectRatioClamp'); + + /// {@macro AcceptableAspectRatios} + final AcceptableAspectRatios aspectRatioClamp; + + /// The actual constraints of the widget, probably provided by a + /// LayoutBuilder. + final BoxConstraints constraints; + + /// The maximum size of the widget. + final Size? maxSize; + + /// Runtime aspect ratio of the actually available space. + double get constraintsAspectRatio => + constraints.maxWidth / constraints.maxHeight; + + /// The given aspect ratio clamped to the allowed range. + double get optimalAspectRatio => + constraintsAspectRatio.clamp(minAspectRatio, maxAspectRatio); + + Size? _optimalSize; + + /// Returns the optimal [Size] for the desired aspect ratio and runtime + /// [BoxConstraints]. + Size get optimalSize { + if (_optimalSize == null) { + if (optimalAspectRatio > constraintsAspectRatio) { + // Optimal size is wider than available space; width is the constraint + _optimalSize = Size( + constraints.maxWidth, + constraints.maxWidth / optimalAspectRatio, + ); + } else { + // Available space is wider than the optimal aspect ratio; height is + // the constraint + _optimalSize = Size( + constraints.maxHeight * optimalAspectRatio, + constraints.maxHeight, + ); + } + + if (maxSize != null) { + // Find the scale required to fit inside maxSize without changing the + // aspect ratio. We only scale down (scale < 1.0). + final scaleX = maxSize!.width / _optimalSize!.width; + final scaleY = maxSize!.height / _optimalSize!.height; + final double minScale = min(1, min(scaleX, scaleY)); + + _optimalSize = _optimalSize! * minScale; + } + } + assert(() { + if (!_lte(_optimalSize!.height, constraints.maxHeight)) { + throw AssertionError( + 'Calculated scaledSize height exceeded constraints. ' + '${_optimalSize!.height} > ${constraints.maxHeight}', + ); + } + if (!_lte(_optimalSize!.width, constraints.maxWidth)) { + throw AssertionError( + 'Calculated scaledSize width exceeded constraints. ' + ' ${_optimalSize!.width} > ${constraints.maxWidth}', + ); + } + if (maxSize != null) { + if (!_lte(_optimalSize!.height, maxSize!.height)) { + throw AssertionError( + 'Calculated scaledSize height exceeded maxSize. ' + '${_optimalSize!.height} > ${maxSize!.height}', + ); + } + if (!_lte(_optimalSize!.width, maxSize!.width)) { + throw AssertionError( + 'Calculated scaledSize width exceeded maxSize. ' + '${_optimalSize!.width} > ${maxSize!.width}', + ); + } + } + return true; + }(), 'Invalid optimalSize'); + return _optimalSize!; + } + + /// Scales a given height value. + double scaleHeight(double designHeight) => optimalSize.height / designHeight; + + /// Scales a given width value. + double scaleWidth(double designWidth) => optimalSize.width / designWidth; + + /// Scales a given size value against the most constraining of the two + /// dimensions. + double scale(Size size) => + min(scaleWidth(size.width), scaleHeight(size.height)); + + /// The axis that should be used for sizing. If provided by the constructor, + /// then that axis will be used. Otherwise, the axis will be determined by + /// which dimension is closer to its ideal size. + /// + /// When the [constraintsOrientation] is [LayoutOrientation.portrait], then + /// width is the most constrained, which means all sizes are scaled based on + /// the ratio of available width to ideal width. + LayoutOrientation get constraintsOrientation => + _constraintsOrientation ??= constraintsAspectRatio.orientation; + + LayoutOrientation? _constraintsOrientation; + + /// The lowest allowed aspect ratio below which values must be clamped on the + /// most constraining dimension, which is width. + double get minAspectRatio => aspectRatioClamp.$1 ?? idealAspectRatio; + + /// The ideal aspect ratio, which is used to calculate the ideal height and + /// width. + double get idealAspectRatio => aspectRatioClamp.$2 ?? constraintsAspectRatio; + + /// The highest allowed aspect ratio above which values must be clamped on the + /// most constraining dimension, which is height. + double get maxAspectRatio => aspectRatioClamp.$3 ?? idealAspectRatio; +} + +extension on double { + /// Returns the orientation of the aspect ratio. + LayoutOrientation get orientation { + return switch (this) { + < 1 => .portrait, + > 1 => .landscape, + == 1 => .square, + _ => throw UnimplementedError('This is impossible'), + }; + } +} + +/// Returns true if [lower] is less than or equal to [higher] with [delta] +/// tolerance for floating point inaccuracies. +bool _lte(double lower, double higher, [double delta = 0.001]) { + return (higher > lower) || (higher - lower) <= delta; +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/sneak_peak_carousel.dart b/genlatte/genlatte-ui/lib/src/widgets/sneak_peak_carousel.dart new file mode 100644 index 0000000..79228cf --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/sneak_peak_carousel.dart @@ -0,0 +1,223 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Horizontal carousel that shows a sneak peek of the next and previous items. +class SneakPeakCarousel extends StatefulWidget { + /// Instantiates a new [SneakPeakCarousel]. + const SneakPeakCarousel({ + required this.children, + this.previewPercentage = 0.35, + this.activeIndex, + this.onIndexChanged, + super.key, + }); + + /// The widgets to show in the carousel + final List children; + + /// The percentage of total width to show for adjacent items + final double previewPercentage; + + /// The currently active index + final int? activeIndex; + + /// Called when the active index changes + final ValueChanged? onIndexChanged; + + @override + State createState() => _SneakPeakCarouselState(); +} + +class _SneakPeakCarouselState extends State { + final ScrollController _scrollController = ScrollController(); + final FocusNode _focusNode = FocusNode(); + late int _currentIndex; + + double? _lastCardWidth; + double? _lastTotalWidth; + + @override + void initState() { + super.initState(); + _currentIndex = widget.activeIndex ?? 0; + } + + @override + void didUpdateWidget(SneakPeakCarousel oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.activeIndex != null && + widget.activeIndex != oldWidget.activeIndex) { + _currentIndex = widget.activeIndex!; + if (_lastCardWidth != null && _lastTotalWidth != null) { + _scrollToIndex( + _currentIndex, + _lastCardWidth!, + _lastTotalWidth!, + ); + } + } + } + + @override + void dispose() { + _focusNode.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _scrollToIndex( + int targetIndex, + double cardWidth, + double totalWidth, { + bool animate = true, + }) { + if (targetIndex < 0 || targetIndex >= widget.children.length) return; + + if (!_scrollController.hasClients) return; + + final offset = targetIndex * cardWidth; + + if (animate) { + _scrollController + .animateTo( + offset, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + ) + .then((_) { + if (mounted) { + setState(() { + _currentIndex = targetIndex; + }); + } + }) + .ignore(); + } else { + _scrollController.jumpTo(offset); + setState(() { + _currentIndex = targetIndex; + }); + } + } + + void _handlePanUpdate(DragUpdateDetails details) { + final dx = details.delta.dx; + _scrollController.jumpTo(_scrollController.offset - dx); + } + + void _handlePanEnd( + DragEndDetails details, + double cardWidth, + double totalWidth, + ) { + if (!_scrollController.hasClients) return; + + final velocity = details.primaryVelocity ?? 0; + int targetIndex = _currentIndex; + + if (velocity < -150) { + targetIndex = _currentIndex + 1; + } else if (velocity > 150) { + targetIndex = _currentIndex - 1; + } else { + // Snap to nearest index based on offset. + targetIndex = (_scrollController.offset / cardWidth).round(); + } + + // Bounds check + targetIndex = targetIndex.clamp(0, widget.children.length - 1); + + if (targetIndex != _currentIndex) { + widget.onIndexChanged?.call(targetIndex); + setState(() { + _currentIndex = targetIndex; + }); + } + + _scrollToIndex(targetIndex, cardWidth, totalWidth); + } + + void _handleArrowKeyEvent(int delta, double cardWidth, double totalWidth) { + final targetIndex = (_currentIndex + delta).clamp( + 0, + widget.children.length - 1, + ); + + if (targetIndex != _currentIndex) { + widget.onIndexChanged?.call(targetIndex); + setState(() { + _currentIndex = targetIndex; + }); + _scrollToIndex(targetIndex, cardWidth, totalWidth); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final totalWidth = constraints.maxWidth; + // The card takes the remaining width + final cardWidth = totalWidth * (1 - widget.previewPercentage); + + if (_lastTotalWidth == null && _currentIndex != 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToIndex( + _currentIndex, + cardWidth, + totalWidth, + animate: false, + ); + }); + } + + _lastTotalWidth = totalWidth; + _lastCardWidth = cardWidth; + + return Focus( + focusNode: _focusNode, + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + _handleArrowKeyEvent(-1, cardWidth, totalWidth); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + _handleArrowKeyEvent(1, cardWidth, totalWidth); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: GestureDetector( + onHorizontalDragUpdate: _handlePanUpdate, + onHorizontalDragEnd: (details) => + _handlePanEnd(details, cardWidth, totalWidth), + child: ListView.builder( + controller: _scrollController, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.symmetric( + horizontal: (totalWidth - cardWidth) / 2, + ), + scrollDirection: Axis.horizontal, + itemCount: widget.children.length, + itemBuilder: (context, index) { + return SizedBox( + width: cardWidth, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: widget.children[index], + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/stacked_images.dart b/genlatte/genlatte-ui/lib/src/widgets/stacked_images.dart new file mode 100644 index 0000000..3881770 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/stacked_images.dart @@ -0,0 +1,50 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +/// A widget that stacks two widgets (typically images) on top of each other. +/// +/// The [top] widget is centered over the [bottom] widget and is scaled +/// according to [topScaleRatio]. +class StackedImages extends StatelessWidget { + /// Creates a [StackedImages] widget. + const StackedImages({ + required this.bottom, + required this.top, + this.topScaleRatio = 0.8, + super.key, + }); + + /// The widget at the bottom of the stack. This widget determines the + /// overall size of the [StackedImages] widget. + final Widget bottom; + + /// The widget centered on top of the [bottom] widget. + final Widget top; + + /// The scale ratio of the [top] widget relative to the [bottom] widget's + /// size. + /// + /// For example, `0.8` means the [top] widget will be 80% the size of the + /// [bottom] widget. + final double topScaleRatio; + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + bottom, + Positioned.fill( + child: FractionallySizedBox( + widthFactor: topScaleRatio, + heightFactor: topScaleRatio, + child: top, + ), + ), + ], + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/triple_tap.dart b/genlatte/genlatte-ui/lib/src/widgets/triple_tap.dart new file mode 100644 index 0000000..29e0d92 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/triple_tap.dart @@ -0,0 +1,86 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Widget which calls its [onPressed] callback after three taps within the +/// [duration]. +class TripleTapDetector extends StatefulWidget { + /// Instantiates a TripleTapDetector. + const TripleTapDetector({ + required this.child, + required this.onPressed, + this.semanticLabel, + this.semanticHint, + this.duration = const Duration(milliseconds: 800), + super.key, + }); + + /// Descendant widget to display. + final Widget child; + + /// The callback to invoke after 3 rapid taps. + final VoidCallback onPressed; + + /// A description of what the widget is (e.g., "Developer Menu"). + final String? semanticLabel; + + /// A description of what happens when activated (e.g., "Triple tap to open"). + final String? semanticHint; + + /// The maximum amount of time allowed from the first tap to the third tap. + final Duration duration; + + @override + State createState() => _TripleTapDetectorState(); +} + +class _TripleTapDetectorState extends State { + int _tapCount = 0; + DateTime? _firstTapTime; + + void _handleTap() { + final now = DateTime.now(); + + // If it's the first tap or the state was reset + if (_tapCount == 0 || _firstTapTime == null) { + _firstTapTime = now; + _tapCount = 1; + } else { + // Check if the current tap is within the allowed duration window + if (now.difference(_firstTapTime!) <= widget.duration) { + _tapCount++; + + if (_tapCount == 3) { + widget.onPressed(); // Fire the callback + _tapCount = 0; // Reset the counter for the next sequence + } + } else { + // The time window expired. Treat this tap as the first tap of a new + // sequence. + _firstTapTime = now; + _tapCount = 1; + } + } + } + + @override + Widget build(BuildContext context) { + return Semantics( + // Identifies this widget as an interactive button to screen readers + button: true, + label: widget.semanticLabel, + hint: widget.semanticHint ?? 'Triple tap to activate', + onTap: widget.onPressed, + child: GestureDetector( + // We use behavior: HitTestBehavior.opaque to ensure the GestureDetector + // catches taps even if the child doesn't fill the entire space or is + // transparent. + behavior: HitTestBehavior.opaque, + onTap: _handleTap, + child: widget.child, + ), + ); + } +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/typography.dart b/genlatte/genlatte-ui/lib/src/widgets/typography.dart new file mode 100644 index 0000000..ad56412 --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/typography.dart @@ -0,0 +1,15 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// GenLatte specific text styles. All unspecified text should use default +/// shadcn_flutter definitions. +extension GenLatteTextExtension on Text { + /// GenLatte version of [h2] which does not have a bottom border. + TextModifier get h2_ => WrappedText( + style: (context, theme) => theme.typography.h2, + child: this, + ); +} diff --git a/genlatte/genlatte-ui/lib/src/widgets/widgets.dart b/genlatte/genlatte-ui/lib/src/widgets/widgets.dart new file mode 100644 index 0000000..de9278f --- /dev/null +++ b/genlatte/genlatte-ui/lib/src/widgets/widgets.dart @@ -0,0 +1,22 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'buttons.dart'; +export 'chevron_button.dart'; +export 'clipboard_value.dart'; +export 'configuration_card.dart'; +export 'dynamic_button_layout.dart'; +export 'extensions.dart'; +export 'footer.dart'; +export 'genlatte_scaffold.dart.dart'; +export 'latte_image.dart'; +export 'layout_provider.dart'; +export 'loading_dash.dart'; +export 'machine_selection.dart'; +export 'overflow_marquee.dart'; +export 'responsive_sized_box.dart'; +export 'sneak_peak_carousel.dart'; +export 'stacked_images.dart'; +export 'triple_tap.dart'; +export 'typography.dart'; diff --git a/genlatte/genlatte-ui/pubspec.yaml b/genlatte/genlatte-ui/pubspec.yaml new file mode 100644 index 0000000..2fc3e27 --- /dev/null +++ b/genlatte/genlatte-ui/pubspec.yaml @@ -0,0 +1,96 @@ +name: genlatte +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0+1 + +environment: + flutter: ^3.43.0-0.3.pre + sdk: ^3.11.0-296.4.beta + +dependencies: + clipboard: ^3.0.14 + cloud_firestore: 6.3.0 + cloud_functions: 6.2.0 + data_layer: 0.0.5 + data_layer_hive: ^0.0.2-rc.1 + equatable: ^2.0.8 + firebase_analytics: ^12.3.0 + firebase_auth: 6.4.0 + firebase_core: 4.7.0 + firebase_remote_config: ^6.4.0 + flutter: + sdk: flutter + flutter_animate: ^4.5.2 + flutter_bloc: ^9.1.1 + flutter_shaders: ^0.1.3 + flutter_svg: ^2.2.4 + flutter_web_plugins: + sdk: flutter + freezed_annotation: ^3.1.0 + genlatte_data: + path: ../genlatte-data + get_it: ^9.2.0 + go_router: ^17.0.1 + google_fonts: ^8.0.0 + hashlib: ^2.3.0 + hive_ce: ^2.19.3 + hive_ce_flutter: ^2.3.4 + json_annotation: ^4.10.0 + logging: ^1.3.0 + material_symbols_icons: ^4.2928.1 + meta: ^1.17.0 + provider: ^6.1.5+1 + shadcn_flutter: ^0.0.52 + shared_preferences: ^2.5.5 + stream_transform: ^2.1.0 + text_responsive: ^1.2.1 + thanos_snap_effect: ^0.0.9 + uuid: ^4.5.3 + video_player: ^2.11.1 + web_browser_detect: ^2.3.0 + +dev_dependencies: + bloc_test: ^10.0.0 + build_runner: ^2.10.5 + fake_cloud_firestore: 4.1.0+1 + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + freezed: ^3.2.4 + hive_ce_generator: ^1.11.0 + json_serializable: ^6.12.0 + mocktail: ^1.0.4 + network_image_mock: ^2.1.1 + very_good_analysis: ^10.0.0 + +flutter: + uses-material-design: true + + assets: + - assets/firebase-logo.png + - assets/flutter-latte.png + - assets/flutter-logo.png + - assets/gemini-logo.png + - assets/green-check.svg + - assets/io-primary-gradient-mesh-small.jpg + - assets/latte-background.png + - assets/latte-background-thumb.png + + - assets/videos/Dash_Loading_1080px.webm + - assets/videos/Dash_Loading_1080px_H.265.mov + + # Avatars + - assets/avatars/asianFemale.png + - assets/avatars/asianMale.png + - assets/avatars/blackFemale.png + - assets/avatars/blackMale.png + - assets/avatars/caucasianFemale.png + - assets/avatars/caucasianMale.png + - assets/avatars/indianFemale.png + - assets/avatars/indianMale.png + - assets/avatars/hispanicFemale.png + - assets/avatars/hispanicMale.png + + shaders: + - assets/shaders/squish.glsl + - packages/thanos_snap_effect/shader/thanos_snap_effect.glsl diff --git a/genlatte/genlatte-ui/scripts/CONTEXT.md b/genlatte/genlatte-ui/scripts/CONTEXT.md new file mode 100644 index 0000000..7574177 --- /dev/null +++ b/genlatte/genlatte-ui/scripts/CONTEXT.md @@ -0,0 +1,24 @@ +# UI Scripts Module (`genlatte-ui/scripts/`) + +## Purpose +This directory contains convenience scripts and symlinks specifically for the `genlatte-ui` Flutter package development. It allows developers working within the UI package to access project-wide automation without navigating back to the project root. + +## Detailed File Overviews + +- **`reset_ui.sh` (Symlink)**: + - **Description**: A relative symbolic link to the main workspace reset script at `../../scripts/reset_ui.sh`. + - **Usage**: Provides a local entry point for clearing the UI sandbox and taking state snapshots. + +## Dependencies/Relationships + +- **Root Scripts**: Dynamically linked to the centralized project-wide scripts. +- **Git**: Complements the development workflow by ensuring the sandbox environment remains in a fresh Git state. + +## Usage/Exports + +### Running the Reset Script +You can execute the workspace reset directly from within this directory: +```bash +./scripts/reset_ui.sh +``` +The script is robust and will automatically identify the repository root regardless of the call location. diff --git a/genlatte/genlatte-ui/scripts/reset_ui.sh b/genlatte/genlatte-ui/scripts/reset_ui.sh new file mode 120000 index 0000000..9a7a67a --- /dev/null +++ b/genlatte/genlatte-ui/scripts/reset_ui.sh @@ -0,0 +1 @@ +../../scripts/reset_ui.sh \ No newline at end of file diff --git a/genlatte/genlatte-ui/test/CONTEXT.md b/genlatte/genlatte-ui/test/CONTEXT.md new file mode 100644 index 0000000..6f36052 --- /dev/null +++ b/genlatte/genlatte-ui/test/CONTEXT.md @@ -0,0 +1,12 @@ +# UI Package Tests + +**Purpose:** +The centralized test suite for the `genlatte-ui` Flutter package, ensuring the reliability of data models, widgets, and screen logic. + +**Subdirectories:** +- `models/`: Shared model unit tests. +- `widgets/`: Shared widget golden/interaction tests. +- `src/`: Deep logic tests mirroring the `lib/src` structure. + +**Dependencies/Relationships:** +- Utilizes `flutter_test` and `mocktail` for verification. diff --git a/genlatte/genlatte-ui/test/models/CONTEXT.md b/genlatte/genlatte-ui/test/models/CONTEXT.md new file mode 100644 index 0000000..9bdbd1e --- /dev/null +++ b/genlatte/genlatte-ui/test/models/CONTEXT.md @@ -0,0 +1,10 @@ +# Global Model Tests + +**Purpose:** +Contains unit tests for shared data models used within the UI package. + +**Detailed File Overviews:** +- `test_model.dart`: A specialized test utility or base class used to facilitate model verification. + +**Dependencies/Relationships:** +- Part of the `genlatte-ui` testing suite. diff --git a/genlatte/genlatte-ui/test/models/test_model.dart b/genlatte/genlatte-ui/test/models/test_model.dart new file mode 100644 index 0000000..0b21243 --- /dev/null +++ b/genlatte/genlatte-ui/test/models/test_model.dart @@ -0,0 +1,40 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:data_layer/data_layer.dart' show Json; +import 'package:flutter/foundation.dart'; + +/// Test model. +@immutable +class TestModel { + /// Instantiates a [TestModel]. + const TestModel({required this.name, this.id}); + + /// Json deserialization constructor. + factory TestModel.fromJson(Json json) => TestModel( + id: json['id'] as String?, + name: json['name'] as String? ?? '', + ); + + /// Id of the object. + final String? id; + + /// Name of the object. + final String name; + + @override + String toString() => 'TestModel(id: $id, name: $name)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TestModel && other.id == id && other.name == name; + } + + @override + int get hashCode => id.hashCode ^ name.hashCode; + + Json toJson() => {'id': id, 'name': name}; +} diff --git a/genlatte/genlatte-ui/test/src/CONTEXT.md b/genlatte/genlatte-ui/test/src/CONTEXT.md new file mode 100644 index 0000000..ad75efd --- /dev/null +++ b/genlatte/genlatte-ui/test/src/CONTEXT.md @@ -0,0 +1,8 @@ +# App Tests Sub-Tree + +**Purpose:** +A structural parent grouping directory that hosts unit and widget tests for the core logic layer. + +**Implementation Details:** +- **Architecture**: Separated into testing `screens/` (for BLoCs and Widget Trees) and testing internal helpers. +- **Child Contexts**: Read child Context.md files to see specific testing strategies. diff --git a/genlatte/genlatte-ui/test/src/core/CONTEXT.md b/genlatte/genlatte-ui/test/src/core/CONTEXT.md new file mode 100644 index 0000000..23bb4e7 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/core/CONTEXT.md @@ -0,0 +1,10 @@ +# Core Logic Tests + +**Purpose:** +Tests the foundational systems of the application, including routing and global configurations. + +**Subdirectories:** +- `routing/`: Redirection and access control verification. + +**Dependencies/Relationships:** +- Validates logic in `genlatte-ui/lib/src/core/`. diff --git a/genlatte/genlatte-ui/test/src/core/routing/CONTEXT.md b/genlatte/genlatte-ui/test/src/core/routing/CONTEXT.md new file mode 100644 index 0000000..b2eab8a --- /dev/null +++ b/genlatte/genlatte-ui/test/src/core/routing/CONTEXT.md @@ -0,0 +1,10 @@ +# Routing Tests + +**Purpose:** +Verifies the application's complex role-based routing and redirection logic. + +**Detailed File Overviews:** +- `redirection_test.dart`: A critical suite of tests that simulates different user authentication states and roles (Barista, Kiosk, etc.) to ensure that the `GoRouterRedirector` correctly enforces path boundaries. + +**Dependencies/Relationships:** +- Validates logic in `genlatte-ui/lib/src/core/routing/redirection.dart`. diff --git a/genlatte/genlatte-ui/test/src/core/routing/redirection_test.dart b/genlatte/genlatte-ui/test/src/core/routing/redirection_test.dart new file mode 100644 index 0000000..37d5be8 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/core/routing/redirection_test.dart @@ -0,0 +1,705 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genlatte/src/core/auth/auth_bloc.dart'; +import 'package:genlatte/src/core/routing/redirection.dart'; +import 'package:genlatte/src/core/routing/router.dart'; +import 'package:genlatte/src/core/routing/routes.dart'; +import 'package:genlatte/src/role.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAuthBloc extends MockBloc implements AuthBloc {} + +class MockUser extends Mock implements User {} + +void main() { + late MockAuthBloc mockAuthBloc; + late MockUser mockUser; + + setUp(() { + mockAuthBloc = MockAuthBloc(); + mockUser = MockUser(); + when(() => mockUser.uid).thenReturn('test_uid'); + }); + + group('Redirection Tests', () { + // Helper to create AppRouter for a specific role + AppRouter createAppRouter() { + return AppRouter(authBloc: mockAuthBloc); + } + + // Helper to test redirection logic using GoRouterRedirector directly + // This part tests the core logic in isolation + void testRedirector({ + required Role? role, + required bool authenticated, + required String currentPath, + required String? expectedRedirect, + bool ignoreQueryParameters = true, + }) { + final redirector = GoRouterRedirector(); + final authState = authenticated + ? AuthState(user: mockUser, role: role) + : const AuthState(); + final routeState = RouteState(uri: Uri.parse(currentPath)); + + final result = redirector.redirect( + routeState: routeState, + authState: authState, + ); + + String? resultWithoutQueryParams; + if (ignoreQueryParameters && result != null) { + final uri = Uri.tryParse(result); + if (uri != null) { + resultWithoutQueryParams = Uri( + path: uri.path, + // Omit query parameters. + ).toString(); + } + } + + expect( + resultWithoutQueryParams ?? result, + expectedRedirect, + reason: + 'Role $role: Authenticated=$authenticated, Current=$currentPath ' + 'should redirect to $expectedRedirect', + ); + } + + group('Barista', () { + const role = Role.barista; + + test('Unauthenticated user at /home should redirect to /login', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/home', + expectedRedirect: '/login', + ); + }); + + test('Unauthenticated user at /login should NOT redirect', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/login', + expectedRedirect: null, + ); + }); + + test('Authenticated user at /login should redirect to /home', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/login', + expectedRedirect: '/barista', + ); + }); + + test('Authenticated user at /home should NOT redirect', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/home', + expectedRedirect: null, + ); + }); + + test('Authenticated but no role at /home should redirect to /login', () { + testRedirector( + role: null, + authenticated: true, + currentPath: '/home', + expectedRedirect: '/login', + ); + }); + }); + + group('Kiosk', () { + const role = Role.kiosk; + + test('Unauthenticated user at /home should redirect to /login', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/home', + expectedRedirect: '/login', + ); + }); + + test('Unauthenticated user at /login should NOT redirect', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/login', + expectedRedirect: null, + ); + }); + + test('Authenticated user at /login should redirect to /home', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/login', + expectedRedirect: '/kiosk', + ); + }); + + test('Authenticated user at /home should NOT redirect', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/home', + expectedRedirect: null, + ); + }); + }); + + group('QueueObserver', () { + const role = Role.queueObserver; + + test('Unauthenticated user at /home should redirect to /login', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/home', + expectedRedirect: '/login', + ); + }); + + test('Unauthenticated user at /login should NOT redirect', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/login', + expectedRedirect: null, + ); + }); + + test('Authenticated user at /login should redirect to /home', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/login', + expectedRedirect: '/queue', + ); + }); + + test('Authenticated user at /home should NOT redirect', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/home', + expectedRedirect: null, + ); + }); + }); + + group('RecentOrdersObserver', () { + const role = Role.recentOrdersObserver; + + test('Unauthenticated user at /home should redirect to /login', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/home', + expectedRedirect: '/login', + ); + }); + + test('Unauthenticated user at /login should NOT redirect', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/login', + expectedRedirect: null, + ); + }); + + test('Authenticated user at /login should redirect to /home', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/login', + expectedRedirect: '/recentOrders', + ); + }); + + test('Authenticated user at /home should NOT redirect', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/home', + expectedRedirect: null, + ); + }); + }); + + group('Moderator', () { + const role = Role.moderator; + + test('Unauthenticated user at /home should redirect to /login', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/home', + expectedRedirect: '/login', + ); + }); + + test('Unauthenticated user at /login should NOT redirect', () { + testRedirector( + role: role, + authenticated: false, + currentPath: '/login', + expectedRedirect: null, + ); + }); + + test('Authenticated user at /login should redirect to /home', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/login', + expectedRedirect: '/moderator', + ); + }); + + test('Authenticated user at /home should NOT redirect', () { + testRedirector( + role: role, + authenticated: true, + currentPath: '/home', + expectedRedirect: null, + ); + }); + }); + + test('AppRouter listens to AuthBloc stream and redirects', () async { + final endState = AuthState(user: mockUser, role: .barista); + final authController = StreamController(); + + // Start unauthenticated. + when(() => mockAuthBloc.state).thenReturn(AuthState.initial()); + // Then authenticate. + when(() => mockAuthBloc.stream).thenAnswer((_) => authController.stream); + + final router = createAppRouter() + // Simulate GoRouter having completed its initial routing. + ..lastRouteState = RouteState.fromRoute(AppRoutes.initialRoute); + + authController.add(endState); + + // Expect a redirect to /barista. + await expectLater(router.allRedirects, emits('/barista')); + expect(router.lastRouteState!.path, '/barista'); + + await authController.close(); + }); + + test( + 'AppRouter creates continue parameter when protecting route', + () async { + final targetUri = Uri( + path: AppRoutes.queueRoute.path, + queryParameters: {'total': '3'}, + ); + final authController = StreamController(); + + // Start unauthenticated. + when(() => mockAuthBloc.state).thenReturn(AuthState.initial()); + when( + () => mockAuthBloc.stream, + ).thenAnswer((_) => authController.stream); + + final router = createAppRouter() + // Simulate the user attempting to navigate directly + // to a protected route with query parameters. + ..lastRouteState = RouteState(uri: targetUri); + + // Trigger the stream listener with the unauthenticated state. + authController.add(AuthState.initial()); + + final expectedUri = Uri( + path: AppRoutes.loginRoute.path, + queryParameters: {'continue': targetUri.toString()}, + ).toString(); + await expectLater(router.allRedirects, emits(expectedUri)); + + await authController.close(); + }, + ); + + test( + 'AppRouter uses continue parameter after successful login', + () async { + final endState = AuthState(user: mockUser, role: .queueObserver); + final targetUri = Uri( + path: AppRoutes.queueRoute.path, + queryParameters: {'total': '3'}, + ); + final authController = StreamController(); + + // Start unauthenticated. + when(() => mockAuthBloc.state).thenReturn(AuthState.initial()); + when( + () => mockAuthBloc.stream, + ).thenAnswer((_) => authController.stream); + + final router = createAppRouter() + // Simulate the user having been redirected to login. + ..lastRouteState = RouteState( + uri: Uri( + path: AppRoutes.loginRoute.path, + queryParameters: {'continue': targetUri.toString()}, + ), + ); + + // Trigger authenticated state. + authController.add(endState); + + await expectLater(router.allRedirects, emits(targetUri.toString())); + + await authController.close(); + }, + ); + + group('NonBaristaAwayFromBaristaHome', () { + const targetPath = '/barista'; + + test('Barista user at /barista should NOT redirect', () { + testRedirector( + role: Role.barista, + authenticated: true, + currentPath: targetPath, + expectedRedirect: null, + ); + }); + + test('Kiosk user at /barista should redirect to /kiosk', () { + testRedirector( + role: Role.kiosk, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/kiosk', + ); + }); + + test('QueueObserver user at /barista should redirect to /queue', () { + testRedirector( + role: Role.queueObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/queue', + ); + }); + + test( + 'RecentOrdersObserver user at /barista should redirect to /recentOrders', + () { + testRedirector( + role: Role.recentOrdersObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/recentOrders', + ); + }, + ); + + test('Anonymous user at /barista should redirect to /login', () { + testRedirector( + role: null, + authenticated: false, + currentPath: targetPath, + expectedRedirect: '/login', + ); + }); + }); + + group('NonKioskAwayFromKioskHome', () { + const targetPath = '/kiosk'; + + test('Kiosk user at /kiosk should NOT redirect', () { + testRedirector( + role: Role.kiosk, + authenticated: true, + currentPath: targetPath, + expectedRedirect: null, + ); + }); + + test('Barista user at /kiosk should redirect to /barista', () { + testRedirector( + role: Role.barista, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/barista', + ); + }); + + test('QueueObserver user at /kiosk should redirect to /queue', () { + testRedirector( + role: Role.queueObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/queue', + ); + }); + + test( + 'RecentOrdersObserver user at /kiosk should redirect to /recentOrders', + () { + testRedirector( + role: Role.recentOrdersObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/recentOrders', + ); + }, + ); + + test('Anonymous user at /kiosk should redirect to /login', () { + testRedirector( + role: null, + authenticated: false, + currentPath: targetPath, + expectedRedirect: '/login', + ); + }); + }); + + group('NonQueueObserverAwayFromQueue', () { + const targetPath = '/queue'; + + test('QueueObserver user at /queue should NOT redirect', () { + testRedirector( + role: Role.queueObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: null, + ); + }); + + test('Barista user at /queue should redirect to /barista', () { + testRedirector( + role: Role.barista, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/barista', + ); + }); + + test('Kiosk user at /queue should redirect to /kiosk', () { + testRedirector( + role: Role.kiosk, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/kiosk', + ); + }); + + test( + 'RecentOrdersObserver user at /queue should redirect to /recentOrders', + () { + testRedirector( + role: Role.recentOrdersObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/recentOrders', + ); + }, + ); + + test('Anonymous user at /queue should redirect to /login', () { + testRedirector( + role: null, + authenticated: false, + currentPath: targetPath, + expectedRedirect: '/login', + ); + }); + }); + + group('NonRecentOrdersObserverAwayFromRecentOrders', () { + const targetPath = '/recentOrders'; + + test( + 'RecentOrdersObserver user at /recentOrders should NOT redirect', + () { + testRedirector( + role: Role.recentOrdersObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: null, + ); + }, + ); + + test('Barista user at /recentOrders should redirect to /barista', () { + testRedirector( + role: Role.barista, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/barista', + ); + }); + + test('Kiosk user at /recentOrders should redirect to /kiosk', () { + testRedirector( + role: Role.kiosk, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/kiosk', + ); + }); + + test('QueueObserver user at /recentOrders should redirect to /queue', () { + testRedirector( + role: Role.queueObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/queue', + ); + }); + + test('Anonymous user at /recentOrders should redirect to /login', () { + testRedirector( + role: null, + authenticated: false, + currentPath: targetPath, + expectedRedirect: '/login', + ); + }); + }); + + group('NonModeratorObserverAwayFromModeration', () { + const targetPath = '/moderator'; + + test('Moderator user at /moderator should NOT redirect', () { + testRedirector( + role: Role.moderator, + authenticated: true, + currentPath: targetPath, + expectedRedirect: null, + ); + }); + + test('Barista user at /moderator should redirect to /barista', () { + testRedirector( + role: Role.barista, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/barista', + ); + }); + + test('Kiosk user at /moderator should redirect to /kiosk', () { + testRedirector( + role: Role.kiosk, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/kiosk', + ); + }); + + test('QueueObserver user at /moderator should redirect to /queue', () { + testRedirector( + role: Role.queueObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/queue', + ); + }); + + test( + 'RecentOrdersObserver user at /moderator should redirect to /recentOrders', + () { + testRedirector( + role: Role.recentOrdersObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/recentOrders', + ); + }, + ); + + test('Anonymous user at /moderator should redirect to /login', () { + testRedirector( + role: null, + authenticated: false, + currentPath: targetPath, + expectedRedirect: '/login', + ); + }); + }); + + group('NonModeratorsAwayFromPrinters', () { + const targetPath = '/machines'; + + test('Moderator user at /machines should NOT redirect', () { + testRedirector( + role: Role.moderator, + authenticated: true, + currentPath: targetPath, + expectedRedirect: null, + ); + }); + + test('Barista user at /machines should redirect to /barista', () { + testRedirector( + role: Role.barista, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/barista', + ); + }); + + test('Kiosk user at /machines should redirect to /kiosk', () { + testRedirector( + role: Role.kiosk, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/kiosk', + ); + }); + + test('QueueObserver user at /machines should redirect to /queue', () { + testRedirector( + role: Role.queueObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/queue', + ); + }); + + test( + 'RecentOrdersObserver user at /machines should redirect to /recentOrders', + () { + testRedirector( + role: Role.recentOrdersObserver, + authenticated: true, + currentPath: targetPath, + expectedRedirect: '/recentOrders', + ); + }, + ); + + test('Anonymous user at /machines should redirect to /login', () { + testRedirector( + role: null, + authenticated: false, + currentPath: targetPath, + expectedRedirect: '/login', + ); + }); + }); + }); +} diff --git a/genlatte/genlatte-ui/test/src/screens/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/CONTEXT.md new file mode 100644 index 0000000..4633437 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/CONTEXT.md @@ -0,0 +1,25 @@ +# Screen Tests + +**Purpose:** +Provides automated UI and State Unit tests for the various high-level views (Barista, Kiosk, Moderator, Queue, and Recent Orders) of the `genlatte-ui` package, ensuring state integrity and visual regression protection. + +**Directory Structure Overview:** + +- `barista/`: + - Contains BLoC and Widget tests covering the `BaristaHomeBloc` and the Order Queue lists to ensure baristas can successfully claim and complete orders from mocked Firebase sources. + +- `kiosk/`: + - Validates the `KioskHomeBloc` multi-step state machine, ensuring the wizard advances forward and triggers image generation callbacks correctly when dummy user data is supplied. + +- `moderator/`: + - Tests ensuring moderators possess the ability to toggle approval statuses via the `ModeratorHomeBloc` and that Machine validation forms work correctly in isolation. + +- `queue/`: + - Complex tests assessing the mathematical properties of the `QueueHomeBloc`, verifying that independent local Sharding correctly filters mock `LatteOrder` documents based on hashing modulo logic, and validating page flips on `OrderList`. + +- `recent/`: + - Contains visual layout tests asserting the 3-Layer bubble system does not overflow and that `RecentOrdersHomeBloc` correctly sorts and limits the returned `RecentLatteImage` streams dynamically based on viewport configuration. + +**Dependencies/Relationships:** +- Actively consumes pure and mocked versions of classes from `genlatte_data` (like `LatteOrder`, `Barista`, `LatteOrderMetadata`). +- Targets `genlatte-ui/lib/src/screens/` logic. diff --git a/genlatte/genlatte-ui/test/src/screens/barista/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/barista/CONTEXT.md new file mode 100644 index 0000000..d7ed28c --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/barista/CONTEXT.md @@ -0,0 +1,10 @@ +# Barista Feature Tests + +**Purpose:** +Tests for the Barista fulfillment features. + +**Subdirectories:** +- `home/`: BLoC verification for fulfillment workflows. + +**Dependencies/Relationships:** +- Validates logic in `genlatte-ui/lib/src/screens/barista/`. diff --git a/genlatte/genlatte-ui/test/src/screens/barista/home/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/barista/home/CONTEXT.md new file mode 100644 index 0000000..4ed35a5 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/barista/home/CONTEXT.md @@ -0,0 +1,10 @@ +# Barista Home Tests + +**Purpose:** +Unit tests for the Barista's fulfillment workflow business logic. + +**Detailed File Overviews:** +- `barista_home_bloc_test.dart`: Verifies the `BaristaHomeBloc`, ensuring correct handling of order prioritization and fulfillment state transitions. + +**Dependencies/Relationships:** +- Validates the BLoC in `genlatte-ui/lib/src/screens/barista/home/`. diff --git a/genlatte/genlatte-ui/test/src/screens/barista/home/barista_home_bloc_test.dart b/genlatte/genlatte-ui/test/src/screens/barista/home/barista_home_bloc_test.dart new file mode 100644 index 0000000..920db5a --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/barista/home/barista_home_bloc_test.dart @@ -0,0 +1,309 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genlatte/src/data/data.dart' hide CompleteOrder; +import 'package:genlatte/src/screens/barista/home/barista_home_bloc.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockBaristaRepository extends Mock implements Repository {} + +class MockLatteOrderMetadataRepository extends Mock + implements Repository {} + +class MockLatteOrdersRepository extends Mock implements LatteOrdersRepository {} + +class MockMachineRepository extends Mock implements Repository {} + +class FakeRequestDetails extends Fake implements RequestDetails {} + +class FakeBarista extends Fake implements Barista {} + +void main() { + setUpAll(() { + registerFallbackValue(FakeRequestDetails()); + registerFallbackValue(const LatteOrderMetadata(id: 'fake')); + registerFallbackValue(FakeBarista()); + }); + + group('BaristaHomeBloc', () { + late MockBaristaRepository baristaRepository; + late MockLatteOrderMetadataRepository metadataRepository; + late MockLatteOrdersRepository ordersRepository; + late MockMachineRepository machinesRepository; + + late StreamController> baristasController; + late StreamController> brewController; + late StreamController> machinesController; + + setUp(() { + baristaRepository = MockBaristaRepository(); + metadataRepository = MockLatteOrderMetadataRepository(); + ordersRepository = MockLatteOrdersRepository(); + machinesRepository = MockMachineRepository(); + + baristasController = StreamController>.broadcast(); + brewController = StreamController>.broadcast(); + machinesController = StreamController>.broadcast(); + + when( + () => ordersRepository.sendToPrinters( + imagePath: any(named: 'imagePath'), + machineName: any(named: 'machineName'), + ), + ).thenAnswer((_) async => {}); + + when( + () => baristaRepository.watchList(details: any(named: 'details')), + ).thenAnswer((_) => baristasController.stream); + + // Default mock for initial Local fetch + when( + () => baristaRepository.getItems(details: any(named: 'details')), + ).thenAnswer((_) async => []); + + when( + () => machinesRepository.watchList(details: any(named: 'details')), + ).thenAnswer((_) => machinesController.stream); + + // Default mock for initial Local fetch + when( + () => machinesRepository.getItems(details: any(named: 'details')), + ).thenAnswer((_) async => []); + + when( + () => metadataRepository.watchList( + details: any( + named: 'details', + that: isA().having( + (r) => r.filter, + 'filter', + isA(), + ), + ), + ), + ).thenAnswer((_) => brewController.stream); + + GetIt.I.registerSingleton>(baristaRepository); + GetIt.I.registerSingleton>( + metadataRepository, + ); + GetIt.I.registerSingleton(ordersRepository); + GetIt.I.registerSingleton>(machinesRepository); + }); + + tearDown(() async { + await baristasController.close(); + await brewController.close(); + await machinesController.close(); + await GetIt.I.reset(); + }); + + test('initial state is correct', () async { + final bloc = BaristaHomeBloc(); + expect(bloc.state, BaristaHomeState.initial()); + await bloc.close(); + }); + + blocTest( + 'NewBrewQueueOrders updates brewQueue', + build: () { + when(() => ordersRepository.toLattes(any())).thenAnswer( + (_) async => [ + Latte( + order: const LatteOrder(id: '1', name: 'order1'), + metadata: LatteOrderMetadata( + id: '1', + orderSubmittedTime: DateTime(2024), + ), + ), + ], + ); + return BaristaHomeBloc(); + }, + act: (bloc) => bloc.add( + NewBrewQueueOrders([ + LatteOrderMetadata( + id: '1', + orderSubmittedTime: DateTime(2024), + ), + ]), + ), + expect: () => [ + BaristaHomeState.initial().copyWith( + brewQueue: [ + Latte( + order: const LatteOrder(id: '1', name: 'order1'), + metadata: LatteOrderMetadata( + id: '1', + orderSubmittedTime: DateTime(2024), + ), + ), + ], + ), + ], + verify: (_) { + verify(() => ordersRepository.toLattes(any())).called(1); + }, + ); + + blocTest( + 'NewBaristas updates baristas map', + build: BaristaHomeBloc.new, + act: (bloc) => bloc.add( + const NewBaristas([ + Barista(id: '1', persona: BaristaPersona.asianFemale, username: 'b1'), + ]), + ), + expect: () => [ + BaristaHomeState.initial().copyWith( + baristas: { + '1': const Barista( + id: '1', + persona: BaristaPersona.asianFemale, + username: 'b1', + ), + }, + ), + ], + ); + + blocTest( + 'BaristaSignIn handles new barista', + build: () { + when( + () => baristaRepository.getItems(details: any(named: 'details')), + ).thenAnswer((_) async => []); + when(() => baristaRepository.setItem(any())).thenAnswer( + (_) async => const Barista( + id: 'new-id', + persona: BaristaPersona.asianFemale, + username: 'b1', + ), + ); + when( + () => baristaRepository.setItems( + any(), + any( + that: isA().having( + (r) => r.filter, + 'filter', + isA(), + ), + ), + ), + ).thenAnswer((_) async => []); + + return BaristaHomeBloc(); + }, + act: (bloc) => bloc.add( + const BaristaSignIn( + Barista(persona: BaristaPersona.asianFemale, username: 'b1'), + ), + ), + expect: () => [ + BaristaHomeState.initial().copyWith( + currentBarista: const Barista( + id: 'new-id', + persona: BaristaPersona.asianFemale, + username: 'b1', + ), + ), + ], + verify: (_) { + verify(() => baristaRepository.setItem(any())).called(1); + verify(() => baristaRepository.setItems(any(), any())).called(1); + }, + ); + + blocTest( + 'ClaimOrder updates metadata correctly', + build: () { + when( + () => metadataRepository.setItem(any()), + ).thenAnswer((_) async => const LatteOrderMetadata(id: '1')); + return BaristaHomeBloc(); + }, + seed: () => BaristaHomeState.initial().copyWith( + selectedMachine: const Machine(id: 'm-id', name: 'm1'), + currentBarista: const Barista( + id: 'b-id', + persona: BaristaPersona.asianFemale, + username: 'b1', + ), + brewQueue: [ + Latte( + order: const LatteOrder(id: 'O-1', name: 'order1'), + metadata: LatteOrderMetadata( + id: '1', + imageUrl: 'abc', + orderSubmittedTime: DateTime(2024), + ), + ), + ], + ), + act: (bloc) => bloc.add(const ClaimOrder('O-1')), + verify: (_) { + verify( + () => metadataRepository.setItem( + any( + that: isA() + .having((m) => m.baristaId, 'baristaId', 'b-id') + .having( + (m) => m.status, + 'status', + LatteOrderStatus.inProgress, + ), + ), + ), + ).called(1); + }, + ); + + blocTest( + 'CompleteOrder calls OrderRepository.completeOrder', + build: () { + when( + () => ordersRepository.completeOrder( + orderId: any(named: 'orderId'), + baristaId: any(named: 'baristaId'), + ), + ).thenAnswer((_) async {}); + return BaristaHomeBloc(); + }, + seed: () => BaristaHomeState.initial().copyWith( + currentBarista: const Barista( + id: 'b-id', + persona: BaristaPersona.asianFemale, + username: 'barista1', + ), + brewQueue: [ + Latte( + order: const LatteOrder(id: 'O-1', name: 'order1'), + metadata: LatteOrderMetadata( + id: '1', + imageUrl: 'abc', + orderSubmittedTime: DateTime(2024), + ), + ), + ], + ), + act: (bloc) => bloc.add(const CompleteOrder('O-1')), + verify: (_) { + verify( + () => ordersRepository.completeOrder( + orderId: 'O-1', + baristaId: 'b-id', + ), + ).called(1); + }, + ); + }); +} diff --git a/genlatte/genlatte-ui/test/src/screens/kiosk/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/kiosk/CONTEXT.md new file mode 100644 index 0000000..9f08541 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/kiosk/CONTEXT.md @@ -0,0 +1,10 @@ +# Kiosk Feature Tests + +**Purpose:** +Tests for the Kiosk ordering features and user interactions. + +**Subdirectories:** +- `home/`: BLoC and View verification for the ordering flow. + +**Dependencies/Relationships:** +- Validates logic in `genlatte-ui/lib/src/screens/kiosk/`. diff --git a/genlatte/genlatte-ui/test/src/screens/kiosk/home/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/kiosk/home/CONTEXT.md new file mode 100644 index 0000000..6059c5e --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/kiosk/home/CONTEXT.md @@ -0,0 +1,11 @@ +# Kiosk Home Tests + +**Purpose:** +Comprehensive verification of the Kiosk customer journey. + +**Detailed File Overviews:** +- `kiosk_home_bloc_test.dart`: Unit tests for the state machine managing the ordering process (e.g., preference selection, art generation). +- `kiosk_home_view_test.dart`: Widget tests that verify the UI transitions and user interaction flows within the Kiosk's step-based home view. + +**Dependencies/Relationships:** +- Validates the logic and UI in `genlatte-ui/lib/src/screens/kiosk/home/`. diff --git a/genlatte/genlatte-ui/test/src/screens/kiosk/home/kiosk_home_bloc_test.dart b/genlatte/genlatte-ui/test/src/screens/kiosk/home/kiosk_home_bloc_test.dart new file mode 100644 index 0000000..77162d0 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/kiosk/home/kiosk_home_bloc_test.dart @@ -0,0 +1,964 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genlatte/src/data/data.dart' as repo; +import 'package:genlatte/src/screens/kiosk/home/kiosk_home_bloc.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; + +class FakeRemoteSource extends LocalMemorySource with WatchableSource { + FakeRemoteSource({required super.bindings}); + + final Map>> _watchControllers = {}; + + void pushUpdate(String id, T? item) { + final result = ReadResult.success( + item, + details: RequestDetails.read(requestType: RequestType.refresh), + ); + if (_watchControllers.containsKey(id)) { + _watchControllers[id]!.add(result); + } else { + // Create it if it doesn't exist yet, so we can eagerly push + _watchControllers[id] = StreamController>.broadcast(); + _watchControllers[id]!.add(result); + } + } + + @override + SourceType get sourceType => SourceType.remote; + + @override + Stream> watch(ReadOperation operation) { + final id = operation.itemId; + if (!_watchControllers.containsKey(id)) { + _watchControllers[id] = StreamController>.broadcast(); + } + return _watchControllers[id]!.stream; + } + + @override + Stream> watchList(ReadListOperation operation) => + Stream>.empty(); + + @override + Stream> watchByIds(ReadByIdsOperation operation) => + Stream>.empty(); +} + +class FakeRepository extends Repository { + factory FakeRepository(Bindings bindings) { + final local = LocalMemorySource(bindings: bindings); + final remote = FakeRemoteSource(bindings: bindings); + final list = SourceList( + bindings: bindings, + sources: [local, remote], + ); + return FakeRepository._( + bindings: bindings, + localSource: local, + remoteSource: remote, + sourceList: list, + ); + } + FakeRepository._({ + required this.bindings, + required this.localSource, + required this.remoteSource, + required SourceList sourceList, + }) : super(sourceList); + + final Bindings bindings; + // ignore: unreachable_from_main + final LocalMemorySource localSource; + final FakeRemoteSource remoteSource; + + @override + Future setItem(T item, [RequestDetails? details]) async { + T itemCopy = item; + if (bindings.getId(itemCopy) == null) { + if (item is LatteOrder) { + itemCopy = item.copyWith(id: 'mock-assigned-id') as T; + } else if (item is LatteOrderMetadata) { + itemCopy = item.copyWith(id: 'mock-assigned-id') as T; + } + } + return super.setItem(itemCopy, details); + } +} + +class FakeLatteOrdersRepository extends FakeRepository + implements repo.LatteOrdersRepository { + factory FakeLatteOrdersRepository( + Bindings bindings, { + repo.AcceptImage? acceptImage, + repo.CompleteOrder? completeOrder, + repo.GenerateRevisedImages? generateRevisedImages, + repo.RejectImageBatch? rejectImageBatch, + repo.SendToPrinters? sendToPrinters, + repo.SubmitOrder? submitOrder, + }) { + final local = LocalMemorySource(bindings: bindings); + final remote = FakeRemoteSource(bindings: bindings); + final list = SourceList( + bindings: bindings, + sources: [local, remote], + ); + return FakeLatteOrdersRepository._( + bindings: bindings, + localSource: local, + remoteSource: remote, + sourceList: list, + acceptImage: + acceptImage ?? + ({required String imageBatchId, required String imageIndex}) async {}, + completeOrder: + completeOrder ?? + ({required String orderId, required String baristaId}) async {}, + generateRevisedImages: + generateRevisedImages ?? + ({ + required String imageBatchId, + required String imageIndex, + required Map answers, + }) async {}, + rejectImageBatch: rejectImageBatch ?? (String id) async {}, + sendToPrinters: + sendToPrinters ?? + ({required String imagePath, required String machineName}) async {}, + submitOrder: submitOrder ?? (String id) async {}, + ); + } + + FakeLatteOrdersRepository._({ + required super.bindings, + required super.localSource, + required super.remoteSource, + required super.sourceList, + required repo.AcceptImage acceptImage, + required repo.CompleteOrder completeOrder, + required repo.GenerateRevisedImages generateRevisedImages, + required repo.RejectImageBatch rejectImageBatch, + required repo.SendToPrinters sendToPrinters, + required repo.SubmitOrder submitOrder, + }) : _acceptImage = acceptImage, + _completeOrder = completeOrder, + _generateRevisedImages = generateRevisedImages, + _rejectImageBatch = rejectImageBatch, + _sendToPrinters = sendToPrinters, + _submitOrder = submitOrder, + super._(); + + @override + Future acceptImage({ + required String imageBatchId, + required String imageIndex, + }) => _acceptImage(imageBatchId: imageBatchId, imageIndex: imageIndex); + final repo.AcceptImage _acceptImage; + + @override + Future completeOrder({ + required String orderId, + required String baristaId, + }) => _completeOrder(orderId: orderId, baristaId: baristaId); + final repo.CompleteOrder _completeOrder; + + @override + Future generateRevisedImages({ + required String imageBatchId, + required String imageIndex, + required Map answers, + }) => _generateRevisedImages( + imageBatchId: imageBatchId, + imageIndex: imageIndex, + answers: answers, + ); + final repo.GenerateRevisedImages _generateRevisedImages; + + @override + Future rejectImageBatch(String imageBatchId) => + _rejectImageBatch(imageBatchId); + final repo.RejectImageBatch _rejectImageBatch; + + @override + Future sendToPrinters({ + required String imagePath, + required String machineName, + }) => _sendToPrinters(imagePath: imagePath, machineName: machineName); + final repo.SendToPrinters _sendToPrinters; + + @override + Future submitOrder(String orderId) => _submitOrder(orderId); + final repo.SubmitOrder _submitOrder; + + @override + Future> toLattes(List metadatas) async { + return []; + } +} + +void main() { + late FakeLatteOrdersRepository orderRepo; + late FakeRepository metadataRepo; + late FakeRepository imagesRepo; + late FakeRepository optionsRepo; + + const questions = [ + Question.multipleChoice( + id: 'time-of-day', + body: 'What time of day is it?', + acceptableAnswers: ['Morning', 'Evening', 'Night'], + ), + Question( + id: 'free-form', + body: 'Any other changes you want to make?', + helpText: 'This is optional', + ), + Question.zeroToOne( + id: 'how-colorful', + body: 'How colorful should the image be?', + minValueLabel: 'Black and white', + maxValueLabel: 'Technicolor', + ), + Question.negativeOneToOne( + id: 'how-chaotic', + body: 'How chaotic should the image be?', + minValueLabel: 'Calm', + maxValueLabel: 'Chaotic', + ), + ]; + + const image1 = LatteImage( + imageUrl: 'image-1.url', + prompt: 'Gemini Gemini AI man, make me an image as fast as you can', + questions: questions, + description: 'A picture of a cat', + ); + const image2 = LatteImage( + imageUrl: 'image-2.url', + prompt: 'Gemini Gemini AI man, make me an image as fast as you can', + questions: questions, + description: 'A picture of a dog', + ); + const image3 = LatteImage( + imageUrl: 'image-3.url', + prompt: 'Gemini Gemini AI man, make me an image as fast as you can', + questions: questions, + description: 'A picture of a bird', + ); + const image4 = LatteImage( + imageUrl: 'image-4.url', + prompt: 'Gemini Gemini AI man, make me an image as fast as you can', + questions: questions, + description: 'A picture of a fish', + ); + + setUp(() { + orderRepo = FakeLatteOrdersRepository(LatteOrder.bindings); + metadataRepo = FakeRepository( + LatteOrderMetadata.bindings, + ); + imagesRepo = FakeRepository(LatteImageBatch.bindings); + optionsRepo = FakeRepository(LatteOptions.bindings); + + GetIt.I.registerSingleton(orderRepo); + GetIt.I.registerSingleton>(metadataRepo); + GetIt.I.registerSingleton>(imagesRepo); + GetIt.I.registerSingleton>(optionsRepo); + }); + + tearDown(() async { + await GetIt.I.reset(); + }); + + group('KioskHomeBloc Initialization', () { + blocTest( + 'loads initial data streams and attempts to load orders', + build: KioskHomeBloc.new, + act: (bloc) async { + // allow streams to be fully subscribed + await Future.delayed(const Duration(milliseconds: 50)); + + optionsRepo.remoteSource.pushUpdate( + 'milks', + const LatteOptions( + id: 'milks', + values: [ + const LatteOption(name: 'Oat'), + const LatteOption(name: 'Almond'), + ], + ), + ); + optionsRepo.remoteSource.pushUpdate( + 'sweeteners', + const LatteOptions( + id: 'sweeteners', + values: [const LatteOption(name: 'Vanilla')], + ), + ); + }, + expect: () => [ + KioskHomeState.initial().copyWith( + milkOptions: [ + const LatteOption(name: 'Oat'), + const LatteOption(name: 'Almond'), + ], + ), + KioskHomeState.initial().copyWith( + milkOptions: [ + const LatteOption(name: 'Oat'), + const LatteOption(name: 'Almond'), + ], + sweetenerOptions: [const LatteOption(name: 'Vanilla')], + ), + ], + ); + }); + + group('KioskHomeBloc LoadPreExistingOrders Edge Cases', () { + blocTest( + 'Deletes all orders if multiple are returned from local cache', + setUp: () async { + await orderRepo.setItem(const LatteOrder(id: '1', name: 'Order 1')); + await orderRepo.setItem(const LatteOrder(id: '2', name: 'Order 2')); + }, + build: KioskHomeBloc.new, + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 50)); + }, + verify: (bloc) async { + final order1 = await orderRepo.getById( + '1', + RequestDetails.read(requestType: RequestType.local), + ); + final order2 = await orderRepo.getById( + '2', + RequestDetails.read(requestType: RequestType.local), + ); + expect(order1, isNull); + expect(order2, isNull); + }, + ); + + blocTest( + 'Deletes order if a single order is returned but metadata is missing', + setUp: () async { + await orderRepo.setItem( + const LatteOrder(id: 'only-one', name: 'But there is no metadata'), + ); + }, + build: KioskHomeBloc.new, + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 50)); + }, + verify: (bloc) async { + final retrieved = await orderRepo.getById( + 'only-one', + RequestDetails.read(requestType: RequestType.local), + ); + expect(retrieved, isNull); + }, + ); + }); + + group('KioskHomeBloc ApplyPreExistingOrder Routing Edge Cases', () { + blocTest( + 'Routing to `.name` if name is null', + setUp: () async { + await orderRepo.setItem(const LatteOrder(id: 'valid-order')); + await metadataRepo.setItem(const LatteOrderMetadata(id: 'valid-order')); + }, + build: KioskHomeBloc.new, + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 50)); + }, + expect: () => [ + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.name, + ), + ], + ); + + blocTest( + 'Routing to `.milkAndSweetener` if milk or sweetener is null', + setUp: () async { + await orderRepo.setItem( + const LatteOrder( + id: 'valid-order', + name: 'Bob', + // milk is null + ), + ); + await metadataRepo.setItem(const LatteOrderMetadata(id: 'valid-order')); + }, + build: KioskHomeBloc.new, + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 50)); + }, + expect: () => [ + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.milkAndSweetener, + ), + ], + ); + + blocTest( + 'Routing to `.happyPlace` if happyPlace is null', + setUp: () async { + await orderRepo.setItem( + const LatteOrder( + id: 'valid-order', + name: 'Bob', + milk: 'Oat', + sweetener: 'Vanilla', + // happyPlace is null + ), + ); + await metadataRepo.setItem(const LatteOrderMetadata(id: 'valid-order')); + }, + build: KioskHomeBloc.new, + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 50)); + }, + expect: () => [ + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.happyPlace, + ), + ], + ); + + blocTest( + 'Routing to `.submitOrder` if metadata has an imageUrl', + setUp: () async { + await orderRepo.setItem( + const LatteOrder( + id: 'valid-order', + name: 'Bob', + milk: 'Oat', + sweetener: 'Vanilla', + happyPlace: 'A beach', + ), + ); + await metadataRepo.setItem( + const LatteOrderMetadata( + id: 'valid-order', + imageUrl: 'https://example.com/image.png', + ), + ); + }, + build: KioskHomeBloc.new, + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 50)); + }, + expect: () => [ + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.submitOrder, + ), + ], + ); + + blocTest( + 'Routing to `.chooseAnImage` if imageBatchId exists but has no parent', + setUp: () async { + await orderRepo.setItem( + const LatteOrder( + id: 'valid-order', + name: 'Bob', + milk: 'Oat', + sweetener: 'Vanilla', + happyPlace: 'A beach', + ), + ); + await metadataRepo.setItem( + const LatteOrderMetadata( + id: 'valid-order', + imageBatchId: 'batch-1', + ), + ); + await imagesRepo.setItem( + const LatteImageBatch( + id: 'batch-1', + orderId: 'valid-order', + image0: image1, + image1: image2, + image2: image3, + image3: image4, + // parent is null + ), + ); + }, + build: KioskHomeBloc.new, + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 50)); + }, + expect: () => [ + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.chooseAnImage, + ), + ], + ); + + blocTest( + 'Routing to `.chooseATweakedImage` if imageBatchId exists and HAS a ' + 'parent', + setUp: () async { + await orderRepo.setItem( + const LatteOrder( + id: 'valid-order', + name: 'Bob', + milk: 'Oat', + sweetener: 'Vanilla', + happyPlace: 'A beach', + ), + ); + await metadataRepo.setItem( + const LatteOrderMetadata( + id: 'valid-order', + imageBatchId: 'batch-2', + ), + ); + await imagesRepo.setItem( + const LatteImageBatch( + id: 'batch-2', + orderId: 'valid-order', + image0: image1, + image1: image2, + image2: image3, + image3: image4, + parent: LatteImageBatchParent( + id: 'batch-1', + imageIndex: '0', + ), + ), + ); + }, + build: KioskHomeBloc.new, + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 50)); + }, + expect: () => [ + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.chooseATweakedImage, + ), + ], + ); + }); + + group('KioskHomeBloc GoBackKioskWizard Edge Cases', () { + blocTest( + 'From .submitOrder with parent -> goes to .chooseATweakedImage', + build: KioskHomeBloc.new, + seed: () => KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.submitOrder, + imagesBatch: const LatteImageBatch( + id: 'batch-2', + orderId: 'valid-order', + image0: image1, + image1: image2, + image2: image3, + image3: image4, + parent: LatteImageBatchParent( + id: 'batch-1', + imageIndex: '0', + ), + ), + ), + act: (bloc) => bloc.add(const GoBackKioskWizard()), + expect: () => [ + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.chooseATweakedImage, + ), + ], + ); + + blocTest( + 'From .submitOrder without parent -> goes to .chooseAnImage', + build: KioskHomeBloc.new, + seed: () => KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.submitOrder, + imagesBatch: const LatteImageBatch( + id: 'batch-1', + orderId: 'valid-order', + image0: image1, + image1: image2, + image2: image3, + image3: image4, + // parent is null + ), + ), + act: (bloc) => bloc.add(const GoBackKioskWizard()), + expect: () => [ + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.chooseAnImage, + ), + ], + ); + + blocTest( + 'Normal steps -> navigates to previous step', + build: KioskHomeBloc.new, + seed: () => KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.happyPlace, + ), + act: (bloc) => bloc.add(const GoBackKioskWizard()), + expect: () => [ + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.milkAndSweetener, + ), + ], + ); + }); + + group('KioskHomeBloc Server Updates & Subscriptions', () { + blocTest( + 'Transition from isHappyPlaceApproved true -> false fires ' + 'HappyPlaceRejected', + build: KioskHomeBloc.new, + act: (bloc) async { + bloc.add( + const ApplyPreExistingOrder( + LatteOrder( + id: '1', + name: 'Bob', + milk: 'Oat', + sweetener: 'Vanilla', + happyPlace: 'beach', + ), + LatteOrderMetadata(id: '1', isHappyPlaceApproved: true), + ), + ); + await Future.delayed(const Duration(milliseconds: 50)); + + metadataRepo.remoteSource.pushUpdate( + '1', + const LatteOrderMetadata( + id: '1', + isHappyPlaceApproved: false, + happyPlaceModerationReason: 'Inappropriate', + ), + ); + await Future.delayed(const Duration(milliseconds: 50)); + }, + skip: 2, + expect: () => [ + isA() + .having( + (s) => s.metadata?.isHappyPlaceApproved, + 'isHappyPlaceApproved', + false, + ) + .having((s) => s.order?.happyPlace, 'happyPlace', null) + .having( + (s) => s.happyPlaceModerationEvent?.reason, + 'reason', + 'Inappropriate', + ), + ], + ); + + blocTest( + 'Changing imageBatchId cancels previous subscription and starts watching ' + 'new batch', + build: KioskHomeBloc.new, + act: (bloc) async { + bloc.add( + const ApplyPreExistingOrder( + LatteOrder(id: '1'), + LatteOrderMetadata(id: '1', imageBatchId: 'batch-1'), + ), + ); + await Future.delayed(const Duration(milliseconds: 50)); + + // push update with new batch Id + metadataRepo.remoteSource.pushUpdate( + '1', + const LatteOrderMetadata(id: '1', imageBatchId: 'batch-2'), + ); + await Future.delayed(const Duration(milliseconds: 50)); + + // push to the new batch stream explicitly + imagesRepo.remoteSource.pushUpdate( + 'batch-2', + const LatteImageBatch( + id: 'batch-2', + orderId: '1', + image0: image1, + image1: image2, + image2: image3, + image3: image4, + ), + ); + await Future.delayed(const Duration(milliseconds: 50)); + }, + skip: 2, + expect: () => [ + isA().having( + (s) => s.imagesBatch?.id, + 'imagesBatch.id', + 'batch-2', + ), + ], + ); + }); + + group('KioskHomeBloc Feature Commands', () { + blocTest( + 'SubmitUserName creates a brand new order if none exists', + build: KioskHomeBloc.new, + act: (bloc) => bloc.add(const SubmitUserName('Alice')), + expect: () => [ + isA().having( + (s) => s.isSubmitting, + 'isSubmitting', + true, + ), + isA() + .having((s) => s.order?.name, 'order.name', 'Alice') + .having((s) => s.order?.id, 'order.id', isNotNull) + .having((s) => s.isSubmitting, 'isSubmitting', true), + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.milkAndSweetener, + ), + isA().having( + (s) => s.isSubmitting, + 'isSubmitting', + false, + ), + ], + ); + + blocTest( + 'SubmitUserName updates existing order if one exists', + build: KioskHomeBloc.new, + seed: () => KioskHomeState.initial().copyWith( + order: const LatteOrder(id: 'existing-id', name: 'Bob'), + ), + act: (bloc) => bloc.add(const SubmitUserName('Alice')), + expect: () => [ + isA().having( + (s) => s.isSubmitting, + 'isSubmitting', + true, + ), + isA() + .having((s) => s.order?.name, 'order.name', 'Alice') + .having((s) => s.order?.id, 'order.id', 'existing-id') + .having((s) => s.isSubmitting, 'isSubmitting', true), + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.milkAndSweetener, + ), + isA().having( + (s) => s.isSubmitting, + 'isSubmitting', + false, + ), + ], + ); + + blocTest( + 'AnswerQuestion correctly immutably updates the specific question', + build: KioskHomeBloc.new, + seed: () => KioskHomeState.initial().copyWith( + questions: const [ + Question.multipleChoice( + id: 'q1', + body: 'Size?', + acceptableAnswers: ['Small', 'Large'], + ), + Question(id: 'q2', body: 'Name?'), + ], + ), + act: (bloc) => bloc.add( + const AnswerQuestion( + Question.multipleChoice( + id: 'q1', + body: 'Size?', + acceptableAnswers: ['Small', 'Large'], + ), + 'Large', + ), + ), + expect: () => [ + isA() + .having( + (s) => s.questions[0], + 'first question', + isA().having( + (q) => q.selectedValue, + 'selectedValue', + 'Large', + ), + ) + .having( + (s) => s.questions[1], + 'second question', + isA().having((q) => q.answer, 'answer', null), + ), + ], + ); + + blocTest( + 'GenerateRevisedImages sets isSubmitting to true and completes without ' + 'error', + build: KioskHomeBloc.new, + seed: () => KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.tweakImage, + imagesBatch: const LatteImageBatch( + id: 'batch-2', + orderId: 'valid-order', + image0: image1, + image1: image2, + image2: image3, + image3: image4, + ), + selectedImageIndex: 1, + questions: const [ + Question(id: 'q1', body: 'Q?'), + ], + ), + act: (bloc) => bloc.add(const GenerateRevisedImages()), + expect: () => [ + isA().having( + (s) => s.isSubmitting, + 'isSubmitting', + true, + ), + isA().having( + (s) => s.imagesBatch, + 'imagesBatch', + isNull, + ), + isA().having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.chooseATweakedImage, + ), + isA().having( + (s) => s.isSubmitting, + 'isSubmitting', + false, + ), + ], + ); + + blocTest( + 'RejectImageBatch sets isSubmitting to true and watches parent batch', + build: KioskHomeBloc.new, + seed: () => KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.chooseATweakedImage, + imagesBatch: const LatteImageBatch( + id: 'batch-2', + orderId: 'valid-order', + image0: image1, + image1: image2, + image2: image3, + image3: image4, + parent: LatteImageBatchParent( + id: 'batch-1', + imageIndex: '0', + ), + ), + ), + act: (bloc) => bloc.add(const RejectImageBatch()), + skip: 2, + expect: () => [ + isA() + .having( + (s) => s.currentStep, + 'currentStep', + KioskWizardStep.chooseAnImage, + ) + .having((s) => s.isSubmitting, 'isSubmitting', false), + ], + ); + }); + + group('KioskHomeBloc Race Condition Protection', () { + blocTest( + 'Preserves local milk & sweetener choice if server update arrives with ' + 'null values while at .milkAndSweetener step', + build: KioskHomeBloc.new, + seed: () => KioskHomeState.initial().copyWith( + order: const LatteOrder(id: 'race-order', name: 'Bob'), + currentStep: KioskWizardStep.milkAndSweetener, + ), + act: (bloc) async { + // 1. Establish the stream by watching the order + // In the real bloc, this happens in SubmitUserName or + // ApplyPreExistingOrder. + // We'll use the private _watchOrder if we can, or just trigger an + // apply. + bloc.add( + const ApplyPreExistingOrder( + LatteOrder(id: 'race-order', name: 'Bob'), + LatteOrderMetadata(id: 'race-order'), + ), + ); + // Allow stream to be established + await Future.delayed(const Duration(milliseconds: 10)); + + // 2. User selects milk and sweetener locally + bloc + ..add(const SelectMilk('Oat')) + ..add(const SelectSweetener('Vanilla')); + + // 3. Simulate server update arriving with null values for those fields + orderRepo.remoteSource.pushUpdate( + 'race-order', + const LatteOrder( + id: 'race-order', + name: 'Bob', + ), // milk and sweetener are null + ); + + // Allow some time for the stream and microtasks + await Future.delayed(const Duration(milliseconds: 50)); + }, + // Skip the initial ApplyPreExistingOrder states + skip: 1, + expect: () => [ + // Expect state from SelectMilk + isA().having((s) => s.order?.milk, 'milk', 'Oat'), + // Expect state from SelectSweetener + isA() + .having((s) => s.order?.milk, 'milk', 'Oat') + .having((s) => s.order?.sweetener, 'sweetener', 'Vanilla'), + // The server update should NOT emit a new state that changes these + // back to null. + // It might not emit any state if the resulting order is deemed + // identical to current state. + ], + verify: (bloc) { + expect(bloc.state.order?.milk, equals('Oat')); + expect(bloc.state.order?.sweetener, equals('Vanilla')); + }, + ); + }); +} diff --git a/genlatte/genlatte-ui/test/src/screens/kiosk/home/kiosk_home_view_test.dart b/genlatte/genlatte-ui/test/src/screens/kiosk/home/kiosk_home_view_test.dart new file mode 100644 index 0000000..b4c9c5c --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/kiosk/home/kiosk_home_view_test.dart @@ -0,0 +1,301 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genlatte/src/screens/kiosk/home/kiosk_home_bloc.dart'; +import 'package:genlatte/src/screens/kiosk/home/kiosk_home_view.dart'; +import 'package:genlatte/src/screens/kiosk/home/steps/steps.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +class MockKioskHomeBloc extends MockBloc + implements KioskHomeBloc {} + +void main() { + late MockKioskHomeBloc mockBloc; + + setUp(() { + mockBloc = MockKioskHomeBloc(); + when(() => mockBloc.stream).thenAnswer((_) => const Stream.empty()); + }); + + Widget buildSubject({ + required Size size, + required KioskHomeBloc bloc, + }) { + return ShadcnApp( + title: 'GenLatte', + home: Scaffold( + child: MediaQuery( + data: MediaQueryData(size: size), + child: LayoutBuilder( + builder: (context, constraints) { + return KioskHomeScreen(bloc: bloc); + }, + ), + ), + ), + ); + } + + group('KioskHomeScreen layout tests', () { + const landscapeSize = Size(1920, 1080); + const portraitSize = Size(1080, 1920); + + for (final size in [landscapeSize, portraitSize]) { + final isLandscape = size.width > size.height; + final orientationName = isLandscape ? 'Landscape' : 'Portrait'; + + group('in $orientationName', () { + testWidgets('renders KioskIntro when on .intro step', (tester) async { + when(() => mockBloc.state).thenReturn( + KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.intro, + ), + ); + + await tester.pumpWidget( + buildSubject(size: size, bloc: mockBloc), + ); + await tester.pumpAndSettle(); + + expect(find.byKey(const ValueKey('step-intro')), findsOneWidget); + }); + + testWidgets('renders KioskDrinkOrderName when on .name step', ( + tester, + ) async { + when(() => mockBloc.state).thenReturn( + KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.name, + order: const LatteOrder(id: '1', name: 'Test User'), + ), + ); + + await tester.pumpWidget( + buildSubject(size: size, bloc: mockBloc), + ); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is KioskDrinkOrderName && widget.name == 'Test User', + ), + findsOneWidget, + ); + }); + + testWidgets( + 'renders KioskDrinkMilkSweetener when on .milkAndSweetener step', + (tester) async { + when(() => mockBloc.state).thenReturn( + KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.milkAndSweetener, + order: const LatteOrder(id: '1', name: 'Test User'), + milkOptions: [ + const LatteOption(name: 'Oat'), + const LatteOption(name: 'Almond'), + ], + sweetenerOptions: [ + const LatteOption(name: 'Vanilla'), + const LatteOption(name: 'None'), + ], + ), + ); + + await tester.pumpWidget( + buildSubject(size: size, bloc: mockBloc), + ); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is KioskDrinkMilkSweetener && + widget.username == 'Test User', + ), + findsOneWidget, + ); + }, + ); + + testWidgets('renders KioskHappyPlace when on .happyPlace step', ( + tester, + ) async { + when(() => mockBloc.state).thenReturn( + KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.happyPlace, + order: const LatteOrder( + id: '1', + name: 'Test User', + happyPlace: 'Mountain top', + ), + ), + ); + + await tester.pumpWidget( + buildSubject(size: size, bloc: mockBloc), + ); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is KioskHappyPlace && + widget.happyPlace == 'Mountain top', + ), + findsOneWidget, + ); + }); + + testWidgets('renders KioskChooseAnImage when on .chooseAnImage step', ( + tester, + ) async { + await mockNetworkImagesFor(() async { + const mockImagesBatch = LatteImageBatch( + id: 'batch1', + orderId: '1', + image0: LatteImage( + imageUrl: 'img0', + prompt: 'opt1', + questions: [], + description: 'd1', + ), + image1: LatteImage( + imageUrl: 'img1', + prompt: 'opt2', + questions: [], + description: 'd2', + ), + image2: LatteImage( + imageUrl: 'img2', + prompt: 'opt3', + questions: [], + description: 'd3', + ), + image3: LatteImage( + imageUrl: 'img3', + prompt: 'opt4', + questions: [], + description: 'd4', + ), + ); + + when(() => mockBloc.state).thenReturn( + KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.chooseAnImage, + imagesBatch: mockImagesBatch, + selectedImageIndex: 0, + ), + ); + + await tester.pumpWidget( + buildSubject(size: size, bloc: mockBloc), + ); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is KioskChooseAnImage && + widget.imagesBatch?.id == 'batch1', + ), + findsOneWidget, + ); + }); + }); + + testWidgets('renders KioskTweakImage when on .tweakImage step', ( + tester, + ) async { + when(() => mockBloc.state).thenReturn( + KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.tweakImage, + selectedImageIndex: 0, + questions: const [ + TextQuestion(id: 'q1', body: 'Which color?'), + TextQuestion(id: 'q2', body: 'Which style?'), + ], + ), + ); + + await tester.pumpWidget( + buildSubject(size: size, bloc: mockBloc), + ); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is KioskTweakImage && widget.questions.length == 2, + ), + findsOneWidget, + ); + }); + + testWidgets('renders KioskSubmitOrder when on .submitOrder step', ( + tester, + ) async { + when(() => mockBloc.state).thenReturn( + KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.submitOrder, + order: const LatteOrder( + id: '1', + name: 'Test User', + sweetener: 'Sugar', + milk: 'Whole', + ), + metadata: const LatteOrderMetadata( + id: '1', + imageUrl: 'path/to/image', + ), + ), + ); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + buildSubject(size: size, bloc: mockBloc), + ); + await tester.pumpAndSettle(); + }); + + expect( + find.byWidgetPredicate( + (widget) => widget is KioskSubmitOrder && widget.order.id == '1', + ), + findsOneWidget, + ); + }); + + testWidgets('renders KioskConfirmation when on .confirmation step', ( + tester, + ) async { + when(() => mockBloc.state).thenReturn( + KioskHomeState.initial().copyWith( + currentStep: KioskWizardStep.confirmation, + metadata: const LatteOrderMetadata(id: '1', orderNumber: 1234), + ), + ); + + await tester.pumpWidget( + buildSubject(size: size, bloc: mockBloc), + ); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is KioskConfirmation && widget.orderNumber == 1234, + ), + findsOneWidget, + ); + }); + }); + } + }); +} diff --git a/genlatte/genlatte-ui/test/src/screens/moderator/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/moderator/CONTEXT.md new file mode 100644 index 0000000..219f8c6 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/moderator/CONTEXT.md @@ -0,0 +1,10 @@ +# Moderator Feature Tests + +**Purpose:** +Tests for the Moderator persona's specific features and business logic. + +**Subdirectories:** +- `home/`: BLoC verification for order moderation. + +**Dependencies/Relationships:** +- Validates logic in `genlatte-ui/lib/src/screens/moderator/`. diff --git a/genlatte/genlatte-ui/test/src/screens/moderator/home/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/moderator/home/CONTEXT.md new file mode 100644 index 0000000..5f18c62 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/moderator/home/CONTEXT.md @@ -0,0 +1,10 @@ +# Moderator Home Tests + +**Purpose:** +Unit tests for the Moderator persona's primary business logic. + +**Detailed File Overviews:** +- `moderator_home_bloc_test.dart`: Verifies the `ModeratorHomeBloc`, ensuring it correctly handles order approval, rejection, and stream merging for the moderation queue. + +**Dependencies/Relationships:** +- Validates the BLoC in `genlatte-ui/lib/src/screens/moderator/home/`. diff --git a/genlatte/genlatte-ui/test/src/screens/moderator/home/moderator_home_bloc_test.dart b/genlatte/genlatte-ui/test/src/screens/moderator/home/moderator_home_bloc_test.dart new file mode 100644 index 0000000..3e9dcf5 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/moderator/home/moderator_home_bloc_test.dart @@ -0,0 +1,292 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genlatte/src/data/data.dart'; +import 'package:genlatte/src/screens/moderator/home/moderator_home_bloc.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockBaristaRepository extends Mock implements Repository {} + +class MockLatteOrderMetadataRepository extends Mock + implements Repository {} + +class MockLatteOrdersRepository extends Mock implements LatteOrdersRepository {} + +class FakeRequestDetails extends Fake implements RequestDetails {} + +void main() { + setUpAll(() { + registerFallbackValue(FakeRequestDetails()); + registerFallbackValue(const LatteOrderMetadata(id: 'fake')); + }); + + group('ModeratorHomeBloc', () { + late MockBaristaRepository baristaRepository; + late MockLatteOrderMetadataRepository metadataRepository; + late MockLatteOrdersRepository ordersRepository; + + late StreamController> baristasController; + late StreamController> brewController; + late StreamController> moderationController; + + setUp(() { + baristaRepository = MockBaristaRepository(); + metadataRepository = MockLatteOrderMetadataRepository(); + ordersRepository = MockLatteOrdersRepository(); + + baristasController = StreamController>.broadcast(); + brewController = StreamController>.broadcast(); + moderationController = + StreamController>.broadcast(); + + when( + () => baristaRepository.watchList(details: any(named: 'details')), + ).thenAnswer((_) => baristasController.stream); + + when( + () => metadataRepository.watchList( + details: any( + named: 'details', + that: isA().having( + (r) => r.filter, + 'filter', + isA(), + ), + ), + ), + ).thenAnswer((_) => brewController.stream); + + when( + () => metadataRepository.watchList( + details: any( + named: 'details', + that: isA().having( + (r) => r.filter, + 'filter', + isA(), + ), + ), + ), + ).thenAnswer((_) => moderationController.stream); + + GetIt.I.registerSingleton>(baristaRepository); + GetIt.I.registerSingleton>( + metadataRepository, + ); + GetIt.I.registerSingleton(ordersRepository); + }); + + tearDown(() async { + await baristasController.close(); + await brewController.close(); + await moderationController.close(); + await GetIt.I.reset(); + }); + + test('initial state is correct', () async { + final bloc = ModeratorHomeBloc(); + expect(bloc.state, ModeratorHomeState.initial()); + await bloc.close(); + }); + + blocTest( + 'NewModerateQueueOrders updates moderationQueue', + build: () { + when(() => ordersRepository.toLattes(any())).thenAnswer( + (_) async => [ + const Latte( + order: LatteOrder(id: '1', name: 'order1'), + metadata: LatteOrderMetadata(id: '1'), + ), + ], + ); + return ModeratorHomeBloc(); + }, + act: (bloc) => bloc.add( + const NewModerateQueueOrders([LatteOrderMetadata(id: '1')]), + ), + expect: () => [ + ModeratorHomeState.initial().copyWith( + moderationQueue: [ + const Latte( + order: LatteOrder(id: '1', name: 'order1'), + metadata: LatteOrderMetadata(id: '1'), + ), + ], + ), + ], + verify: (_) { + verify(() => ordersRepository.toLattes(any())).called(1); + }, + ); + + blocTest( + 'NewBaristas updates baristas map', + build: ModeratorHomeBloc.new, + act: (bloc) => bloc.add( + const NewBaristas([ + Barista(id: '1', persona: BaristaPersona.asianFemale, username: 'b1'), + ]), + ), + expect: () => [ + ModeratorHomeState.initial().copyWith( + baristas: { + '1': const Barista( + id: '1', + persona: BaristaPersona.asianFemale, + username: 'b1', + ), + }, + ), + ], + ); + + blocTest( + 'ApproveNameAndImage updates metadata correctly', + build: () { + when( + () => metadataRepository.setItem(any()), + ).thenAnswer((_) async => const LatteOrderMetadata(id: '1')); + return ModeratorHomeBloc(); + }, + seed: () => ModeratorHomeState.initial().copyWith( + moderationQueue: [ + const Latte( + order: LatteOrder(id: '1', name: 'order1'), + metadata: LatteOrderMetadata(id: '1'), + ), + ], + ), + act: (bloc) => bloc.add(const ApproveNameAndImage('1')), + verify: (_) { + verify( + () => metadataRepository.setItem( + any( + that: isA() + .having((m) => m.isNameApproved, 'isNameApproved', true) + .having((m) => m.isImageApproved, 'isImageApproved', true) + .having( + (m) => m.status, + 'status', + LatteOrderStatus.validated, + ), + ), + ), + ).called(1); + }, + ); + + blocTest( + 'RejectNameApproveImage updates metadata correctly', + build: () { + when( + () => metadataRepository.setItem(any()), + ).thenAnswer((_) async => const LatteOrderMetadata(id: '1')); + return ModeratorHomeBloc(); + }, + seed: () => ModeratorHomeState.initial().copyWith( + moderationQueue: [ + const Latte( + order: LatteOrder(id: '1', name: 'order1'), + metadata: LatteOrderMetadata(id: '1'), + ), + ], + ), + act: (bloc) => bloc.add(const RejectNameApproveImage('1')), + verify: (_) { + verify( + () => metadataRepository.setItem( + any( + that: isA() + .having((m) => m.isNameApproved, 'isNameApproved', false) + .having((m) => m.isImageApproved, 'isImageApproved', true) + .having( + (m) => m.status, + 'status', + LatteOrderStatus.validated, + ), + ), + ), + ).called(1); + }, + ); + + blocTest( + 'ApproveNameRejectImage updates metadata correctly', + build: () { + when( + () => metadataRepository.setItem(any()), + ).thenAnswer((_) async => const LatteOrderMetadata(id: '1')); + return ModeratorHomeBloc(); + }, + seed: () => ModeratorHomeState.initial().copyWith( + moderationQueue: [ + const Latte( + order: LatteOrder(id: '1', name: 'order1'), + metadata: LatteOrderMetadata(id: '1'), + ), + ], + ), + act: (bloc) => bloc.add(const ApproveNameRejectImage('1')), + verify: (_) { + verify( + () => metadataRepository.setItem( + any( + that: isA() + .having((m) => m.isNameApproved, 'isNameApproved', true) + .having((m) => m.isImageApproved, 'isImageApproved', false) + .having( + (m) => m.status, + 'status', + LatteOrderStatus.validated, + ), + ), + ), + ).called(1); + }, + ); + + blocTest( + 'RejectNameAndImage updates metadata correctly', + build: () { + when( + () => metadataRepository.setItem(any()), + ).thenAnswer((_) async => const LatteOrderMetadata(id: '1')); + return ModeratorHomeBloc(); + }, + seed: () => ModeratorHomeState.initial().copyWith( + moderationQueue: [ + const Latte( + order: LatteOrder(id: '1', name: 'order1'), + metadata: LatteOrderMetadata(id: '1'), + ), + ], + ), + act: (bloc) => bloc.add(const RejectNameAndImage('1')), + verify: (_) { + verify( + () => metadataRepository.setItem( + any( + that: isA() + .having((m) => m.isNameApproved, 'isNameApproved', false) + .having((m) => m.isImageApproved, 'isImageApproved', false) + .having( + (m) => m.status, + 'status', + LatteOrderStatus.validated, + ), + ), + ), + ).called(1); + }, + ); + }); +} diff --git a/genlatte/genlatte-ui/test/src/screens/queue/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/queue/CONTEXT.md new file mode 100644 index 0000000..b8c3c0c --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/queue/CONTEXT.md @@ -0,0 +1,10 @@ +# Queue Feature Tests + +**Purpose:** +Tests for the autonomous Queue display features. + +**Subdirectories:** +- `home/`: BLoC verification for sharding and pagination. + +**Dependencies/Relationships:** +- Validates logic in `genlatte-ui/lib/src/screens/queue/`. diff --git a/genlatte/genlatte-ui/test/src/screens/queue/home/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/queue/home/CONTEXT.md new file mode 100644 index 0000000..7a5a15d --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/queue/home/CONTEXT.md @@ -0,0 +1,10 @@ +# Queue Home Tests + +**Purpose:** +Unit tests for the autonomous Queue display logic. + +**Detailed File Overviews:** +- `queue_home_bloc_test.dart`: Exercises the `QueueHomeBloc`, specifically testing its deterministic sharding logic and pagination behavior. + +**Dependencies/Relationships:** +- Validates the BLoC in `genlatte-ui/lib/src/screens/queue/home/`. diff --git a/genlatte/genlatte-ui/test/src/screens/queue/home/queue_home_bloc_test.dart b/genlatte/genlatte-ui/test/src/screens/queue/home/queue_home_bloc_test.dart new file mode 100644 index 0000000..f8efb95 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/queue/home/queue_home_bloc_test.dart @@ -0,0 +1,511 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_redundant_argument_values + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:data_layer/data_layer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genlatte/src/data/data.dart'; +import 'package:genlatte/src/data/shared_preferences_repository.dart'; +import 'package:genlatte/src/screens/queue/home/queue_home_bloc.dart'; +import 'package:genlatte/src/screens/queue/home/queue_settings.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:mocktail/mocktail.dart'; + +void main() { + setUpAll(() { + registerFallbackValue(FakeRequestDetails()); + }); + + group('QueueHomeBloc', () { + late MockLatteOrderMetadataRepository metadataRepository; + late MockLatteOrdersRepository ordersRepository; + late MockSharedPreferencesRepository sharedPrefsRepository; + late StreamController> streamController; + + setUp(() { + metadataRepository = MockLatteOrderMetadataRepository(); + ordersRepository = MockLatteOrdersRepository(); + sharedPrefsRepository = MockSharedPreferencesRepository(); + when( + () => sharedPrefsRepository.setString(any(), any()), + ).thenAnswer((_) async {}); + streamController = StreamController>.broadcast(); + + when( + () => metadataRepository.watchList(details: any(named: 'details')), + ).thenAnswer((_) => streamController.stream); + }); + + tearDown(() async { + await streamController.close(); + }); + + test('initial state is correct', () async { + final bloc = QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 42, + shardTotal: 67, + ), + ); + expect( + bloc.state, + const QueueHomeState( + settings: QueueSettingsState(shardNumber: 42, shardTotal: 67), + ), + ); + await bloc.close(); + }); + + blocTest( + 'SetRecency updates max ages', + build: () => QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 1, + shardTotal: 2, + ), + ), + act: (bloc) => bloc.add( + const SetRecency(Duration(seconds: 10), Duration(seconds: 20)), + ), + expect: () => [ + const QueueHomeState( + settings: QueueSettingsState( + shardNumber: 1, + shardTotal: 2, + maxRecentAge: Duration(seconds: 10), + maxShowAge: Duration(seconds: 20), + ), + ), + ], + ); + + blocTest( + 'SetShard updates shards', + build: () => QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 1, + shardTotal: 2, + ), + ), + act: (bloc) => bloc.add(const SetShard(2, 3)), + expect: () => [ + const QueueHomeState( + settings: QueueSettingsState(shardNumber: 2, shardTotal: 3), + ), + ], + ); + + blocTest( + 'SlotsCounted updates slot capacity', + build: () => QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 1, + shardTotal: 1, + ), + ), + act: (bloc) => bloc.add(const SlotsCounted(42)), + + expect: () => contains( + isA().having( + (s) => s.uiSlotCapacity, + 'uiSlotCapacity', + 42, + ), + ), + ); + + blocTest( + 'Orders load and pages are updated on capacity', + build: () { + when(() => ordersRepository.toLattes(any())).thenAnswer( + (_) async => [ + Latte( + order: const LatteOrder(id: '1', name: 'order1'), + metadata: LatteOrderMetadata( + id: '1', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now().subtract( + const Duration(seconds: 10), + ), + ), + ), + ], + ); + return QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 1, + shardTotal: 1, + ), + ); + }, + seed: () => const QueueHomeState( + settings: QueueSettingsState(shardNumber: 1, shardTotal: 1), + uiSlotCapacity: 2, + ), + act: (bloc) async { + bloc.add( + OrdersUpdated([ + LatteOrderMetadata( + id: '1', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now().subtract( + const Duration(seconds: 10), + ), + ), + ]), + ); + await Future.delayed(const Duration(milliseconds: 10)); + }, + expect: () => [ + isA() + .having((s) => s.shownPages.length, 'pages length', 1) + .having( + (s) => s.shownPages.first.orders.length, + 'orders per page', + 2, + ) + .having( + (s) => s.shownPages.first.orders.first?.order.id, + 'order id', + '1', + ), + ], + ); + + blocTest( + 'Shard change should change the UI pages', + build: () { + when(() => ordersRepository.toLattes(any())).thenAnswer( + (_) async => [ + Latte( + order: const LatteOrder(id: '2', name: 'order2'), + metadata: LatteOrderMetadata( + id: '2', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now(), + ), + ), + ], + ); + return QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 1, + shardTotal: 2, + ), + ); + }, + seed: () => const QueueHomeState( + settings: QueueSettingsState(shardNumber: 1, shardTotal: 2), + uiSlotCapacity: 2, + ), + act: (bloc) async { + bloc.add( + OrdersUpdated([ + LatteOrderMetadata( + id: '2', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now(), + ), + ]), + ); + await Future.delayed(const Duration(milliseconds: 10)); + + bloc.add(const SetShard(42, 1337)); + }, + expect: () => [ + // The intermediate state from OrdersUpdated + isA(), + // Setting shard sets details + isA() + .having((s) => s.settings.shardNumber, 'shardNumber', 42) + .having( + (s) => s.settings.shardTotal, + 'shardTotal', + 1337, + ), + // RefreshPages updates the pages based on new details + isA(), + ], + ); + + blocTest( + 'Recency change should change the UI pages', + build: () { + when(() => ordersRepository.toLattes(any())).thenAnswer( + (_) async => [ + Latte( + order: const LatteOrder(id: '2', name: 'order2'), + metadata: LatteOrderMetadata( + id: '2', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now(), + ), + ), + ], + ); + return QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 1, + shardTotal: 2, + ), + ); + }, + seed: () => const QueueHomeState( + settings: QueueSettingsState(shardNumber: 1, shardTotal: 2), + uiSlotCapacity: 2, + ), + act: (bloc) async { + bloc.add( + OrdersUpdated([ + LatteOrderMetadata( + id: '2', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now(), + ), + ]), + ); + await Future.delayed(const Duration(milliseconds: 10)); + + bloc.add(const SetRecency(Duration(minutes: 3), Duration(minutes: 30))); + }, + expect: () => [ + // First state from OrdersUpdated + isA(), + // Second from setting recency + isA().having( + (s) => s.settings.maxRecentAge, + 'maxRecent', + const Duration(minutes: 3), + ), + // Third from refresh + isA(), + ], + ); + + blocTest( + 'When orders arrive but there are no UI slots, ' + 'it should not try to show the orders', + build: () { + when(() => ordersRepository.toLattes(any())).thenAnswer( + (_) async => [ + Latte( + order: const LatteOrder(id: '3', name: 'order3'), + metadata: LatteOrderMetadata( + id: '3', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now(), + ), + ), + ], + ); + return QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 1, + shardTotal: 1, + ), + ); + }, + seed: () => const QueueHomeState( + settings: QueueSettingsState(shardNumber: 1, shardTotal: 1), + uiSlotCapacity: 0, + ), + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 10)); + + bloc.add( + OrdersUpdated([ + LatteOrderMetadata( + id: '3', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now(), + ), + ]), + ); + await Future.delayed(const Duration(milliseconds: 10)); + }, + expect: () => [], + ); + + blocTest( + 'When there are no UI slots and orders arrive, ' + 'a new SlotsCounted with non-zero slots should produce pages immediately', + build: () { + when(() => ordersRepository.toLattes(any())).thenAnswer( + (_) async => [ + Latte( + order: const LatteOrder(id: '4', name: 'order4'), + metadata: LatteOrderMetadata( + id: '4', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now(), + ), + ), + ], + ); + return QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 1, + shardTotal: 1, + ), + ); + }, + seed: () => const QueueHomeState( + settings: QueueSettingsState(shardNumber: 1, shardTotal: 1), + uiSlotCapacity: 0, + ), + act: (bloc) async { + bloc.add( + OrdersUpdated([ + LatteOrderMetadata( + id: '4', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now(), + ), + ]), + ); + await Future.delayed(const Duration(milliseconds: 10)); + + bloc.add(const SlotsCounted(4)); + }, + expect: () => [ + const QueueHomeState( + settings: QueueSettingsState(shardNumber: 1, shardTotal: 1), + uiSlotCapacity: 4, + ), + isA() + .having((s) => s.uiSlotCapacity, 'capacity', 4) + .having((s) => s.shownPages.length, 'pages length', 1), + ], + ); + + test( + 'sharding splits all orders without duplicates or omissions', + () async { + when(() => ordersRepository.toLattes(any())).thenAnswer((inv) async { + final metadatas = + inv.positionalArguments.first as List; + return metadatas + .map( + (m) => Latte( + order: LatteOrder(id: m.id ?? '', name: 'order'), + metadata: m, + ), + ) + .toList(); + }); + + final metadatas = List.generate( + 100, + (i) => LatteOrderMetadata( + id: 'order_$i', + status: LatteOrderStatus.inProgress, + orderSubmittedTime: DateTime.now(), + ), + ); + + final bloc1 = + QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 1, + shardTotal: 2, + ), + ) + ..add(const SlotsCounted(100)) + ..add(OrdersUpdated(metadatas)); + + final bloc2 = + QueueHomeBloc( + metadataRepository: metadataRepository, + ordersRepository: ordersRepository, + sharedPrefsRepository: sharedPrefsRepository, + initialSettings: const QueueSettingsState( + shardNumber: 2, + shardTotal: 2, + ), + ) + ..add(const SlotsCounted(100)) + ..add(OrdersUpdated(metadatas)); + + await Future.delayed(const Duration(milliseconds: 100)); + + final orders1 = bloc1.state.shownPages + .expand((p) => p.orders) + .whereType() + .map((e) => e.metadata.id!) + .toSet(); + + final orders2 = bloc2.state.shownPages + .expand((p) => p.orders) + .whereType() + .map((e) => e.metadata.id!) + .toSet(); + + expect( + orders1, + isNotEmpty, + reason: 'Should select at least one order from a hundred', + ); + + expect( + orders1.intersection(orders2), + isEmpty, + reason: 'Shards should not share any orders', + ); + + expect( + orders1.union(orders2).length, + 100, + reason: 'Together, shards should show all orders without omission', + ); + + await bloc1.close(); + await bloc2.close(); + }, + ); + }); +} + +class FakeRequestDetails extends Fake implements RequestDetails {} + +class MockLatteOrderMetadataRepository extends Mock + implements Repository {} + +class MockLatteOrdersRepository extends Mock implements LatteOrdersRepository {} + +class MockSharedPreferencesRepository extends Mock + implements SharedPreferencesRepository {} diff --git a/genlatte/genlatte-ui/test/src/screens/recent/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/recent/CONTEXT.md new file mode 100644 index 0000000..0c5ad0a --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/recent/CONTEXT.md @@ -0,0 +1,10 @@ +# Recent Orders Feature Tests + +**Purpose:** +Tests for the physics-based "Recent Orders" display logic. + +**Subdirectories:** +- `models/`: Physics calculation verification. + +**Dependencies/Relationships:** +- Validates logic in `genlatte-ui/lib/src/screens/recent_orders/`. diff --git a/genlatte/genlatte-ui/test/src/screens/recent/models/CONTEXT.md b/genlatte/genlatte-ui/test/src/screens/recent/models/CONTEXT.md new file mode 100644 index 0000000..19d76fe --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/recent/models/CONTEXT.md @@ -0,0 +1,10 @@ +# Recent Orders Models Tests + +**Purpose:** +Verifies the physics engine math utilized by the Recent Orders screen. + +**Detailed File Overviews:** +- `wobble_calculator_test.dart`: Specifically tests the physics calculation logic for the elastic bubble collisions and "wobble" effects of latte images. + +**Dependencies/Relationships:** +- Validates the math in `genlatte-ui/lib/src/screens/recent_orders/models/`. diff --git a/genlatte/genlatte-ui/test/src/screens/recent/models/wobble_calculator_test.dart b/genlatte/genlatte-ui/test/src/screens/recent/models/wobble_calculator_test.dart new file mode 100644 index 0000000..96d24d2 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/screens/recent/models/wobble_calculator_test.dart @@ -0,0 +1,62 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genlatte/src/screens/recent_orders/models/wobble_calculator.dart'; + +void main() { + group('WobbleCalculator', () { + const calculator = WobbleCalculator(wobblesPerContact: 3); + test('should return 0 strength at 0 time', () { + expect(calculator.getWobble(0 / 12).strength, closeTo(0, 0.001)); + }); + + test('should return 0.5 strength while 1/4 of the way through phase 1', () { + expect(calculator.getWobble(1 / 12).strength, closeTo(0.5, 0.001)); + }); + + test('should return 1 strength at the peak of phase 1', () { + expect(calculator.getWobble(2 / 12).strength, closeTo(1, 0.001)); + }); + + test('should return 0.5 strength while 3/4 of the way through phase 1', () { + expect(calculator.getWobble(3 / 12).strength, closeTo(0.5, 0.001)); + }); + + test('should return 0 strength at the end of phase 1', () { + expect(calculator.getWobble(4 / 12).strength, closeTo(0, 0.001)); + }); + + test( + 'should return (0.5 * 3/4) strength while 1/4 of the way through phase 2', + () { + expect( + calculator.getWobble(5 / 12).strength, + closeTo(0.5 * 0.75, 0.001), + ); + }, + ); + + test('should return 0.75 strength at the peak of phase 2', () { + expect( + calculator.getWobble(6 / 12).strength, + closeTo(0.75, 0.001), + ); + }); + + test( + 'should return 0.375 strength while 3/4 of the way through phase 2', + () { + expect( + calculator.getWobble(7 / 12).strength, + closeTo(0.5 * 0.75, 0.001), + ); + }, + ); + + test('should return 0 strength at the end of phase 2', () { + expect(calculator.getWobble(8 / 12).strength, closeTo(0, 0.001)); + }); + }); +} diff --git a/genlatte/genlatte-ui/test/src/sources/CONTEXT.md b/genlatte/genlatte-ui/test/src/sources/CONTEXT.md new file mode 100644 index 0000000..700fe72 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/sources/CONTEXT.md @@ -0,0 +1,10 @@ +# Data Source Integration Tests + +**Purpose:** +Verifies the integration between the application and its remote data providers. + +**Detailed File Overviews:** +- `firestore_source_test.dart`: Exercises the `FirestoreSource`, testing data fetching, stream subscriptions, and error handling when interacting with Cloud Firestore. + +**Dependencies/Relationships:** +- Validates the implementations in `genlatte-ui/lib/src/sources/`. diff --git a/genlatte/genlatte-ui/test/src/sources/firestore_source_test.dart b/genlatte/genlatte-ui/test/src/sources/firestore_source_test.dart new file mode 100644 index 0000000..d556d48 --- /dev/null +++ b/genlatte/genlatte-ui/test/src/sources/firestore_source_test.dart @@ -0,0 +1,383 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart' hide Filter; +import 'package:data_layer/data_layer.dart' hide Source; +import 'package:fake_cloud_firestore/fake_cloud_firestore.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genlatte/src/sources/firestore_filters.dart'; +import 'package:genlatte/src/sources/firestore_source.dart'; + +import '../../models/test_model.dart'; + +// Concrete FirestoreFilter for testing +class TestFilter extends FirestoreFilter { + const TestFilter(this.filterFn); + + final Query Function(Query) filterFn; + + @override + Query apply(Query query) => filterFn(query); + + @override + String get cacheKey => 'test-filter'; + + @override + Map toParams() => {}; + + @override + Json toJson() => {}; +} + +// Unknown Filter for testing unsupported filters +class UnknownFilter extends Filter { + @override + String get cacheKey => 'unknown'; + + @override + Json toJson() => {}; + + @override + Map toParams() => {}; +} + +void main() { + group('FirestoreSource', () { + late FakeFirebaseFirestore firestore; + late FirestoreSource source; + late Bindings bindings; + late CollectionReference collection; + + setUp(() { + firestore = FakeFirebaseFirestore(); + bindings = Bindings( + getDetailUrl: (id) => ApiUrl(path: 'items/$id'), + getListUrl: () => const ApiUrl(path: 'items'), + getId: (item) => item.id, + fromJson: TestModel.fromJson, + toJson: (item) => item.toJson(), + ); + source = FirestoreSource(firestore, bindings: bindings); + collection = firestore.collection('items'); + }); + + group('getById', () { + test('returns success with data when document exists', () async { + final data = {'name': 'Test Item'}; + final docRef = await collection.add(data); + final operation = ReadOperation( + operationId: 'op1', + itemId: docRef.id, + details: RequestDetails.read(), + createdAt: DateTime.now(), + ); + + final result = await source.getById(operation); + + expect(result, isA>()); + final success = result as ReadSuccess; + expect( + success.item, + equals(TestModel.fromJson({'id': docRef.id, ...data})), + ); + }); + + test('returns success with null when document does not exist', () async { + final operation = ReadOperation( + operationId: 'op2', + itemId: 'non-existent', + details: RequestDetails.read(), + createdAt: DateTime.now(), + ); + + final result = await source.getById(operation); + + expect(result, isA>()); + final success = result as ReadSuccess; + expect(success.item, isNull); + }); + }); + + group('getByIds', () { + test('returns items and identifying missing ids', () async { + final doc1 = await collection.add({'name': 'Item 1'}); + final doc2 = await collection.add({'name': 'Item 2'}); + const missingId = 'missing-id'; + + final operation = ReadByIdsOperation( + operationId: 'op3', + itemIds: {doc1.id, doc2.id, missingId}, + details: RequestDetails.read(), + createdAt: DateTime.now(), + ); + + final result = await source.getByIds(operation); + + expect(result, isA>()); + final success = result as ReadListSuccess; + + expect(success.items.length, 2); + expect( + success.items.map((e) => e.id), + containsAll([doc1.id, doc2.id]), + ); + expect(success.missingItemIds, contains(missingId)); + }); + }); + + group('getItems', () { + test('returns all items when no filter provided', () async { + await collection.add({'name': 'Item 1'}); + await collection.add({'name': 'Item 2'}); + + final operation = ReadListOperation( + operationId: 'op4', + details: RequestDetails(), + createdAt: DateTime.now(), + ); + + final result = await source.getItems(operation); + + final success = result as ReadListSuccess; + expect(success.items.length, 2); + }); + + test('applies FirestoreFilter correctly', () async { + await collection.add({'name': 'Item 1'}); + await collection.add({'name': 'Item 2'}); + await collection.add({'name': 'Item 3'}); + + final filter = TestFilter( + (query) => query.where('name', isEqualTo: 'Item 2'), + ); + final operation = ReadListOperation( + operationId: 'op5', + details: RequestDetails(filter: filter), + createdAt: DateTime.now(), + ); + + final result = await source.getItems(operation); + + final success = result as ReadListSuccess; + expect(success.items.length, 1); + expect(success.items.every((i) => i.name == 'Item 2'), isTrue); + }); + + test('throws UnsupportedError for non-FirestoreFilter', () async { + final operation = ReadListOperation( + operationId: 'op6', + details: RequestDetails(filter: UnknownFilter()), + createdAt: DateTime.now(), + ); + + expect( + () => source.getItems(operation), + throwsUnsupportedError, + ); + }); + }); + + group('setItem', () { + test('creates new item when id is null', () async { + const item = TestModel(name: 'New Item'); + final operation = WriteOperation( + operationId: 'op7', + item: item, + details: RequestDetails(), + createdAt: DateTime.now(), + ); + + final result = await source.setItem(operation); + + expect(result, isA>()); + final success = result as WriteSuccess; + + expect(success.item.id, isNotNull); + expect(success.item.name, 'New Item'); + + final stored = await collection.doc(success.item.id).get(); + expect(stored.exists, isTrue); + }); + + test('updates existing item when id is present', () async { + final doc = await collection.add({'name': 'Original'}); + final updatedItem = TestModel.fromJson({ + 'id': doc.id, + 'name': 'Updated', + }); + + final operation = WriteOperation( + operationId: 'op9', + item: updatedItem, + details: RequestDetails(), + createdAt: DateTime.now(), + ); + + final result = await source.setItem(operation); + + expect(result, isA>()); + + final stored = await collection.doc(doc.id).get(); + expect(stored.data()!['name'], 'Updated'); + + // Ensure ID wasn't written as a field (implementation detail check) + expect(stored.data()!['id'], isNull); + }); + }); + + group('cleanData', () { + test('converts root-level Timestamps to ISO 8601 strings', () { + final timestamp = Timestamp.fromDate(DateTime.utc(2024)); + final data = { + 'name': 'Test', + 'createdAt': timestamp, + }; + final cleaned = FirestoreSource.cleanData(data); + expect(cleaned['createdAt'], equals('2024-01-01T00:00:00.000Z')); + }); + + test('converts nested Timestamps in Maps', () { + final timestamp = Timestamp.fromDate(DateTime.utc(2024)); + final data = { + 'name': 'Test', + 'metadata': { + 'updatedAt': timestamp, + }, + }; + // This currently FAILS because it returns early if no Timestamps at + // root. + final cleaned = FirestoreSource.cleanData(data); + expect( + (cleaned['metadata']! as Map)['updatedAt'], + equals('2024-01-01T00:00:00.000Z'), + ); + }); + + test('converts nested Timestamps in Lists', () { + final timestamp = Timestamp.fromDate(DateTime.utc(2024)); + final data = { + 'name': 'Test', + 'history': [ + {'at': timestamp}, + ], + }; + // This also currently FAILS for same reason + final cleaned = FirestoreSource.cleanData(data); + expect( + ((cleaned['history']! as List).first as Map)['at'], + equals('2024-01-01T00:00:00.000Z'), + ); + }); + + test('handles lists with non-Map items', () { + final data = { + 'name': 'Test', + 'tags': ['a', 'b'], + 'createdAt': Timestamp.now(), + }; + // This will FAIL currently because (e as Json) in e.map(...) + expect(() => FirestoreSource.cleanData(data), returnsNormally); + }); + + test('returns same object if empty', () { + final data = {}; + final cleaned = FirestoreSource.cleanData(data); + expect(cleaned, same(data)); + }); + + test('returns same object if no Timestamps found', () { + final data = {'name': 'Test', 'count': 1}; + final cleaned = FirestoreSource.cleanData(data); + expect(cleaned, same(data)); + }); + }); + + group('cleanDataForWrite', () { + test('converts root-level DateTimes to Timestamps', () { + final dateTime = DateTime.utc(2024); + final data = { + 'name': 'Test', + 'createdAt': dateTime, + }; + final cleaned = FirestoreSource.cleanDataForWrite(data); + expect(cleaned['createdAt'], isA()); + expect( + (cleaned['createdAt']! as Timestamp).toDate(), + equals(dateTime.toLocal()), + ); + }); + + test('converts ISO 8601 string DateTimes to Timestamps', () { + final dateTime = DateTime.utc(2024); + final data = { + 'name': 'Test', + 'createdAt': dateTime.toIso8601String(), + }; + final cleaned = FirestoreSource.cleanDataForWrite(data); + expect(cleaned['createdAt'], isA()); + expect( + (cleaned['createdAt']! as Timestamp).toDate(), + equals(dateTime.toLocal()), + ); + }); + + test('converts nested DateTimes in Maps', () { + final dateTime = DateTime.utc(2024); + final data = { + 'name': 'Test', + 'metadata': { + 'updatedAt': dateTime, + }, + }; + final cleaned = FirestoreSource.cleanDataForWrite(data); + expect((cleaned['metadata']! as Map)['updatedAt'], isA()); + expect( + ((cleaned['metadata']! as Map)['updatedAt'] as Timestamp).toDate(), + equals(dateTime.toLocal()), + ); + }); + + test('converts nested DateTimes in Lists', () { + final dateTime = DateTime.utc(2024); + final data = { + 'name': 'Test', + 'history': [ + {'at': dateTime}, + ], + }; + final cleaned = FirestoreSource.cleanDataForWrite(data); + expect( + ((cleaned['history']! as List).first as Map)['at'], + isA(), + ); + expect( + (((cleaned['history']! as List).first as Map)['at'] as Timestamp) + .toDate(), + equals(dateTime.toLocal()), + ); + }); + + test('handles lists with non-Map items', () { + final data = { + 'name': 'Test', + 'tags': ['a', 'b'], + 'createdAt': DateTime.now(), + }; + expect(() => FirestoreSource.cleanDataForWrite(data), returnsNormally); + }); + + test('returns same object if empty', () { + final data = {}; + final cleaned = FirestoreSource.cleanDataForWrite(data); + expect(cleaned, same(data)); + }); + + test('returns same object if no DateTimes found', () { + final data = {'name': 'Test', 'count': 1}; + final cleaned = FirestoreSource.cleanDataForWrite(data); + expect(cleaned, same(data)); + }); + }); + }); +} diff --git a/genlatte/genlatte-ui/test/widgets/CONTEXT.md b/genlatte/genlatte-ui/test/widgets/CONTEXT.md new file mode 100644 index 0000000..42b191f --- /dev/null +++ b/genlatte/genlatte-ui/test/widgets/CONTEXT.md @@ -0,0 +1,10 @@ +# Global Widget Tests + +**Purpose:** +Contains widget-level tests for shared components used across the application. + +**Detailed File Overviews:** +- `aspect_ratio_scalar_test.dart`: Verifies the logic of the `AspectRatioScalar` widget, ensuring correct layout calculations across different viewport dimensions. + +**Dependencies/Relationships:** +- Exercises widgets defined in `genlatte-ui/lib/src/widgets/`. diff --git a/genlatte/genlatte-ui/test/widgets/aspect_ratio_scalar_test.dart b/genlatte/genlatte-ui/test/widgets/aspect_ratio_scalar_test.dart new file mode 100644 index 0000000..411dac0 --- /dev/null +++ b/genlatte/genlatte-ui/test/widgets/aspect_ratio_scalar_test.dart @@ -0,0 +1,385 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genlatte/src/widgets/responsive_sized_box.dart'; + +BoxConstraints _fromAspectRatio(double aspectRatio, {double width = 100}) { + return BoxConstraints.tight(Size(width, width / aspectRatio)); +} + +void main() { + group('ApsectRatioScalar.orientation should', () { + test('return portrait when aspect ratio is less than 1', () { + final scalar = AspectRatioScalar(( + null, + 0.5, // does not matter + null, + ), constraints: _fromAspectRatio(0.5)); + expect(scalar.constraintsOrientation, LayoutOrientation.portrait); + }); + + test('return constraints when no ideal is provided', () { + final scalar = AspectRatioScalar(( + null, + null, + null, + ), constraints: _fromAspectRatio(0.5)); + expect(scalar.constraintsOrientation, LayoutOrientation.portrait); + }); + + test('return landscape when aspect ratio is greater than 1', () { + final scalar = AspectRatioScalar(( + null, + 0.5, // does not matter + null, + ), constraints: _fromAspectRatio(2)); + expect(scalar.constraintsOrientation, LayoutOrientation.landscape); + }); + + test('return landscape when aspect ratio is equal to 1', () { + final scalar = AspectRatioScalar(( + null, + 0.5, // does not matter + null, + ), constraints: _fromAspectRatio(1)); + expect(scalar.constraintsOrientation, LayoutOrientation.square); + }); + }); + + group('AspectRatioScalar.clampedAspectRatio should', () { + test('return the ideal aspect ratio when it is within bounds', () { + final scalar = AspectRatioScalar(( + null, + 0.5, + null, + ), constraints: _fromAspectRatio(0.5)); + expect(scalar.optimalAspectRatio, 0.5); + }); + + test('return the minimum aspect ratio when it is below the minimum', () { + final scalar = AspectRatioScalar(( + null, + 0.5, + null, + ), constraints: _fromAspectRatio(0.25)); + expect(scalar.optimalAspectRatio, 0.5); + }); + + test('return the maximum aspect ratio when it is above the maximum', () { + final scalar = AspectRatioScalar(( + null, + 0.5, + null, + ), constraints: _fromAspectRatio(0.75)); + expect(scalar.optimalAspectRatio, 0.5); + }); + + test('return the actual aspect ratio when it is within range', () { + final scalar = AspectRatioScalar(( + null, + 0.5, + 1, + ), constraints: _fromAspectRatio(0.75)); + expect(scalar.optimalAspectRatio, 0.75); + }); + + test('return the maximum aspect ratio when it is above range with max', () { + final scalar = AspectRatioScalar(( + null, + 0.5, + 1, + ), constraints: _fromAspectRatio(1.5)); + expect(scalar.optimalAspectRatio, 1); + }); + + test( + 'return the minimum aspect ratio when it is below minimum with min', + () { + final scalar = AspectRatioScalar(( + 0.25, + 0.5, + 1, + ), constraints: _fromAspectRatio(0.1)); + expect(scalar.optimalAspectRatio, 0.25); + }, + ); + }); + + group('Given a landscape object, AspectRatioScalar.optimalSize should', () { + test('fit into portrait space', () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Portrait space (exactly 100x300) + constraints: _fromAspectRatio(0.333), + ); + expect(scalar.optimalSize.width, closeTo(100, 0.0001)); + expect(scalar.optimalSize.height, closeTo(50, 0.0001)); + }); + + test('fit into portrait space with max size constraining width', () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Portrait space (exactly 100x300) + constraints: _fromAspectRatio(0.333), + maxSize: const Size(75, 75), + ); + expect(scalar.optimalSize.width, closeTo(75, 0.0001)); + expect(scalar.optimalSize.height, closeTo(37.5, 0.0001)); + }); + + test('fit into portrait space with max size constraining height', () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Portrait space (exactly 100x300) + constraints: _fromAspectRatio(0.333), + maxSize: const Size(300, 25), + ); + expect(scalar.optimalSize.width, closeTo(50, 0.0001)); + expect(scalar.optimalSize.height, closeTo(25, 0.0001)); + }); + + test('fit into landscape space', () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Landscape space (exactly 300x100) + constraints: _fromAspectRatio(3, width: 300), + ); + expect(scalar.constraintsOrientation, LayoutOrientation.landscape); + expect(scalar.optimalSize.width, closeTo(200, 0.0001), reason: 'width'); + expect(scalar.optimalSize.height, closeTo(100, 0.0001), reason: 'height'); + }); + + test('fit into landscape space with max size constraining width', () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Landscape space (exactly 300x100) + constraints: _fromAspectRatio(3, width: 300), + maxSize: const Size(250, 250), + ); + expect(scalar.optimalSize.width, closeTo(200, 0.0001), reason: 'width'); + expect(scalar.optimalSize.height, closeTo(100, 0.0001), reason: 'height'); + }); + + test('fit into landscape space with max size constraining height', () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Landscape space (exactly 300x100) + constraints: _fromAspectRatio(3, width: 300), + maxSize: const Size(300, 100), + ); + expect(scalar.optimalSize.width, closeTo(200, 0.0001), reason: 'width'); + expect(scalar.optimalSize.height, closeTo(100, 0.0001), reason: 'height'); + }); + + test('fit into square space', () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Square space (exactly 100x100) + constraints: _fromAspectRatio(1), + ); + expect(scalar.optimalSize.width, closeTo(100, 0.0001), reason: 'width'); + expect(scalar.optimalSize.height, closeTo(50, 0.0001), reason: 'height'); + }); + + test('fit into square space with max size constraining width', () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Square space (exactly 100x100) + constraints: _fromAspectRatio(1), + maxSize: const Size(90, 90), + ); + expect(scalar.optimalSize.width, closeTo(90, 0.0001), reason: 'width'); + expect(scalar.optimalSize.height, closeTo(45, 0.0001), reason: 'height'); + }); + + test('fit into square space with max size constraining height', () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Square space (exactly 100x100) + constraints: _fromAspectRatio(1), + maxSize: const Size(100, 40), + ); + expect(scalar.optimalSize.width, closeTo(80, 0.0001), reason: 'width'); + expect(scalar.optimalSize.height, closeTo(40, 0.0001), reason: 'height'); + }); + + test( + 'fit into landscape space with max size constraining both, ' + 'width is stricter', + () { + final scalar = AspectRatioScalar( + // Landscape object (e.g., 200x100) + (null, 2, null), + // Landscape space (exactly 300x100) + constraints: _fromAspectRatio(3, width: 300), + // Unconstrained size is 200x100. + // We set max size to 50x50, so both dimensions constrain. + // The scale to fit width is 50/200 = 0.25 + // The scale to fit height is 50/100 = 0.5 + // The width scale is stricter. + maxSize: const Size(50, 50), + ); + expect(scalar.optimalSize.width, closeTo(50, 0.0001)); + expect(scalar.optimalSize.height, closeTo(25, 0.0001)); + }, + ); + }); + + group('Given a portrait object, AspectRatioScalar.optimalSize should', () { + test('fit into portrait space', () { + final scalar = AspectRatioScalar( + // Portrait object (e.g., 50x100) + (null, 0.5, null), + // Portrait space (exactly 50x400) + constraints: _fromAspectRatio(0.125, width: 50), + ); + expect(scalar.constraintsOrientation, LayoutOrientation.portrait); + expect(scalar.optimalSize.width, closeTo(50, 0.0001)); + expect(scalar.optimalSize.height, closeTo(100, 0.0001)); + }); + + test('fit into portrait space with max size constraining width', () { + final scalar = AspectRatioScalar( + // Portrait object (e.g., 50x100) + (null, 0.5, null), + // Portrait space (exactly 50x400) + constraints: _fromAspectRatio(0.125, width: 50), + maxSize: const Size(25, 25), + ); + expect(scalar.constraintsOrientation, LayoutOrientation.portrait); + expect(scalar.optimalSize.width, closeTo(12.5, 0.0001)); + expect(scalar.optimalSize.height, closeTo(25, 0.0001)); + }); + + test('fit into portrait space with max size constraining height', () { + final scalar = AspectRatioScalar( + // Portrait object (e.g., 50x100) + (null, 0.5, null), + // Portrait space (exactly 50x400) + constraints: _fromAspectRatio(0.125, width: 50), + maxSize: const Size(75, 75), + ); + expect(scalar.constraintsOrientation, LayoutOrientation.portrait); + expect(scalar.optimalSize.width, closeTo(37.5, 0.0001)); + expect(scalar.optimalSize.height, closeTo(75, 0.0001)); + }); + + test('fit into landscape space', () { + final scalar = AspectRatioScalar( + // Portrait object (e.g., 50x100) + (null, 0.5, null), + // Portrait space (exactly 300x100) + constraints: _fromAspectRatio(3, width: 300), + ); + expect(scalar.constraintsOrientation, LayoutOrientation.landscape); + expect(scalar.optimalSize.width, closeTo(50, 0.0001)); + expect(scalar.optimalSize.height, closeTo(100, 0.0001)); + }); + + test( + 'fit into landscape space with max size constraining width', + () { + final scalar = AspectRatioScalar( + // Portrait object (e.g., 50x100) + (null, 0.5, null), + // Portrait space (exactly 300x100) + constraints: _fromAspectRatio(3, width: 300), + maxSize: const Size(25, 100), + ); + expect(scalar.constraintsOrientation, LayoutOrientation.landscape); + expect(scalar.optimalSize.width, closeTo(25, 0.0001)); + expect(scalar.optimalSize.height, closeTo(50, 0.0001)); + }, + ); + + test( + 'fit into landscape space with max size constraining height', + () { + final scalar = AspectRatioScalar( + // Portrait object (e.g., 50x100) + (null, 0.5, null), + // Portrait space (exactly 300x100) + constraints: _fromAspectRatio(3, width: 300), + maxSize: const Size(75, 75), + ); + expect(scalar.constraintsOrientation, LayoutOrientation.landscape); + expect(scalar.optimalSize.width, closeTo(37.5, 0.0001)); + expect(scalar.optimalSize.height, closeTo(75, 0.0001)); + }, + ); + + test('fit portrait object when available space is square', () { + final scalar = AspectRatioScalar( + // Portrait object (e.g., 50x100) + (null, 0.5, null), + // Portrait space (exactly 300x300) + constraints: _fromAspectRatio(1, width: 300), + ); + expect(scalar.constraintsOrientation, LayoutOrientation.square); + expect(scalar.optimalSize.width, closeTo(150, 0.0001)); + expect(scalar.optimalSize.height, closeTo(300, 0.0001)); + }); + + test( + 'fit into portrait space with max size constraining both, ' + 'height is stricter', + () { + final scalar = AspectRatioScalar( + // Portrait object (e.g., 50x100) + (null, 0.5, null), + // Portrait space (exactly 50x400) + constraints: _fromAspectRatio(0.125, width: 50), + // Unconstrained size is 50x100. + // We set max size to 30x30, so both dimensions constrain. + // The scale to fit width is 30/50 = 0.6 + // The scale to fit height is 30/100 = 0.3 + // The height scale is stricter. + maxSize: const Size(30, 30), + ); + expect(scalar.optimalSize.width, closeTo(15, 0.0001)); + expect(scalar.optimalSize.height, closeTo(30, 0.0001)); + }, + ); + }); + + group('Given a square object, AspectRatioScalar.optimalSize should', () { + test('fit into square space', () { + final scalar = AspectRatioScalar( + (null, 1.0, null), + constraints: _fromAspectRatio(1), + ); + expect(scalar.optimalSize.width, closeTo(100, 0.0001)); + expect(scalar.optimalSize.height, closeTo(100, 0.0001)); + }); + + test('fit into landscape space', () { + final scalar = AspectRatioScalar( + (null, 1.0, null), + constraints: _fromAspectRatio(2, width: 200), + ); + expect(scalar.optimalSize.width, closeTo(100, 0.0001)); + expect(scalar.optimalSize.height, closeTo(100, 0.0001)); + }); + + test('fit into portrait space', () { + final scalar = AspectRatioScalar( + (null, 1.0, null), + constraints: _fromAspectRatio(0.5, width: 50), + ); + expect(scalar.optimalSize.width, closeTo(50, 0.0001)); + expect(scalar.optimalSize.height, closeTo(50, 0.0001)); + }); + }); +} diff --git a/genlatte/genlatte-ui/web/CONTEXT.md b/genlatte/genlatte-ui/web/CONTEXT.md new file mode 100644 index 0000000..1ccbe40 --- /dev/null +++ b/genlatte/genlatte-ui/web/CONTEXT.md @@ -0,0 +1,10 @@ +# Web Configuration & Entry + +**Purpose:** +Contains the root assets and configurations for the Flutter Web build target. + +**Subdirectories:** +- `icons/`: PWA and browser icon assets. + +**Dependencies/Relationships:** +- Contains `index.html` and `manifest.json` which define the web application's metadata and entry point. diff --git a/genlatte/genlatte-ui/web/favicon.png b/genlatte/genlatte-ui/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/genlatte/genlatte-ui/web/favicon.png differ diff --git a/genlatte/genlatte-ui/web/icons/CONTEXT.md b/genlatte/genlatte-ui/web/icons/CONTEXT.md new file mode 100644 index 0000000..242bd52 --- /dev/null +++ b/genlatte/genlatte-ui/web/icons/CONTEXT.md @@ -0,0 +1,11 @@ +# Progressive Web App Icons + +**Purpose:** +Standardized icon assets required for the Web build, particularly for PWA manifest compliance and browser tab identification. + +**Detailed File Overviews:** +- `Icon-192.png` / `Icon-512.png`: Standard square icons for different resolution requirements. +- `Icon-maskable-192.png` / `Icon-maskable-512.png`: Specific variants for platforms that require maskable icons (e.g., Android). + +**Dependencies/Relationships:** +- Referenced by `genlatte-ui/web/manifest.json`. diff --git a/genlatte/genlatte-ui/web/icons/Icon-192.png b/genlatte/genlatte-ui/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/genlatte/genlatte-ui/web/icons/Icon-192.png differ diff --git a/genlatte/genlatte-ui/web/icons/Icon-512.png b/genlatte/genlatte-ui/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/genlatte/genlatte-ui/web/icons/Icon-512.png differ diff --git a/genlatte/genlatte-ui/web/icons/Icon-maskable-192.png b/genlatte/genlatte-ui/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/genlatte/genlatte-ui/web/icons/Icon-maskable-192.png differ diff --git a/genlatte/genlatte-ui/web/icons/Icon-maskable-512.png b/genlatte/genlatte-ui/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/genlatte/genlatte-ui/web/icons/Icon-maskable-512.png differ diff --git a/genlatte/genlatte-ui/web/index.html b/genlatte/genlatte-ui/web/index.html new file mode 100644 index 0000000..43a019a --- /dev/null +++ b/genlatte/genlatte-ui/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + genlatte + + + + + + + diff --git a/genlatte/genlatte-ui/web/manifest.json b/genlatte/genlatte-ui/web/manifest.json new file mode 100644 index 0000000..b002d1a --- /dev/null +++ b/genlatte/genlatte-ui/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "genlatte", + "short_name": "genlatte", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/genlatte/genlatte-ui/widgetbook/.gitignore b/genlatte/genlatte-ui/widgetbook/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/genlatte/genlatte-ui/widgetbook/.metadata b/genlatte/genlatte-ui/widgetbook/.metadata new file mode 100644 index 0000000..38b5dd4 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/.metadata @@ -0,0 +1,30 @@ +# 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: "a7c4c6bf74749e4562e9f47c762a815201285132" + channel: "beta" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: a7c4c6bf74749e4562e9f47c762a815201285132 + base_revision: a7c4c6bf74749e4562e9f47c762a815201285132 + - platform: web + create_revision: a7c4c6bf74749e4562e9f47c762a815201285132 + base_revision: a7c4c6bf74749e4562e9f47c762a815201285132 + + # 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' diff --git a/genlatte/genlatte-ui/widgetbook/CONTEXT.md b/genlatte/genlatte-ui/widgetbook/CONTEXT.md new file mode 100644 index 0000000..fdfcb35 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/CONTEXT.md @@ -0,0 +1,42 @@ +# UI Component Library (Widgetbook) (`genlatte-ui/widgetbook/`) + +## Purpose +A dedicated Flutter project that serves as a design system and interactive catalog for the `genlatte-ui` package. It utilizes the [Widgetbook](https://widgetbook.io/) framework to allow developers to browse, interact with, and test UI components in isolation across different devices and orientations. + +## Detailed File Overviews + +- **`pubspec.yaml`**: + - **Description**: Defines dependencies for the Widgetbook project. + - **Key Dependencies**: Links to the local `genlatte-ui` package and includes `widgetbook` and `widgetbook_annotation` for generating the UI catalog. + +- **`lib/main.dart`**: + - **Description**: The entry point for the Widgetbook application. + - **Core Logic**: Initializes the `Widgetbook` app, injects the project-wide theme (`ShadcnApp`), and registers the generated directory tree. + +- **`lib/*_preview.dart`**: + - **Description**: Individual story/use-case definitions for components found in the core UI package. + - **Files included**: `chevron_button_preview.dart`, `footer_preview.dart`, `latte_image_preview.dart`, etc. + +- **`lib/main.directories.g.dart`**: + - **Description**: Generated file containing the mapping of all components annotated with `@widgetbook.UseCase`. + +## Dependencies/Relationships + +- **Core Package (`genlatte-ui`)**: Widgetbook directly depends on the core package to import and showcase its widgets. +- **Shadcn Flutter**: Uses the same component library and theme as the main application to ensure visual fidelity. +- **Development Tooling**: Integrated into the development workflow to ensure component visual consistency before integration into the main kiosk or barista apps. + +## Usage/Exports + +### Running Widgetbook +To start the component library locally: +```bash +cd genlatte-ui/widgetbook +flutter run -d chrome +``` + +### Adding New Previews +When adding new widgets to the codebase, create a corresponding `_preview.dart` file in this directory and use the `build_runner` to update the catalog: +```bash +dart run build_runner build --delete-conflicting-outputs +``` diff --git a/genlatte/genlatte-ui/widgetbook/README.md b/genlatte/genlatte-ui/widgetbook/README.md new file mode 100644 index 0000000..28e23a0 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/README.md @@ -0,0 +1,3 @@ +# widgetbook + +A new Flutter project. diff --git a/genlatte/genlatte-ui/widgetbook/analysis_options.yaml b/genlatte/genlatte-ui/widgetbook/analysis_options.yaml new file mode 100644 index 0000000..6bb291a --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/analysis_options.yaml @@ -0,0 +1,8 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + errors: + implementation_imports: ignore + +formatter: + trailing_commas: preserve diff --git a/genlatte/genlatte-ui/widgetbook/assets/CONTEXT.md b/genlatte/genlatte-ui/widgetbook/assets/CONTEXT.md new file mode 100644 index 0000000..a6fcddc --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/assets/CONTEXT.md @@ -0,0 +1,11 @@ +# Widgetbook Assets + +**Purpose:** +Storage for static binary assets used within the Widgetbook previews. + +**Subdirectories:** +- `videos/`: Loading screen video previews. +- `shaders/`: Custom shader previews. + +**Dependencies/Relationships:** +- Declared in `genlatte-ui/widgetbook/pubspec.yaml`. diff --git a/genlatte/genlatte-ui/widgetbook/assets/firebase-logo.png b/genlatte/genlatte-ui/widgetbook/assets/firebase-logo.png new file mode 100644 index 0000000..5524d8d Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/assets/firebase-logo.png differ diff --git a/genlatte/genlatte-ui/widgetbook/assets/flutter-logo.png b/genlatte/genlatte-ui/widgetbook/assets/flutter-logo.png new file mode 100644 index 0000000..4d545bd Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/assets/flutter-logo.png differ diff --git a/genlatte/genlatte-ui/widgetbook/assets/gemini-logo.png b/genlatte/genlatte-ui/widgetbook/assets/gemini-logo.png new file mode 100644 index 0000000..6126770 Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/assets/gemini-logo.png differ diff --git a/genlatte/genlatte-ui/widgetbook/assets/latte-background.png b/genlatte/genlatte-ui/widgetbook/assets/latte-background.png new file mode 100644 index 0000000..77cef4c Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/assets/latte-background.png differ diff --git a/genlatte/genlatte-ui/widgetbook/assets/shaders/CONTEXT.md b/genlatte/genlatte-ui/widgetbook/assets/shaders/CONTEXT.md new file mode 100644 index 0000000..48b0c5d --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/assets/shaders/CONTEXT.md @@ -0,0 +1,10 @@ +# Widgetbook Shader Assets + +**Purpose:** +Mirrors the application's shader assets for use within Widgetbook component previews. + +**Detailed File Overviews:** +- `squish.glsl`: Used to preview shader-based effects (like `SquishWidget`) in Widgetbook. + +**Dependencies/Relationships:** +- Referenced by `genlatte-ui/widgetbook/lib/squish_widget_preview.dart`. diff --git a/genlatte/genlatte-ui/widgetbook/assets/shaders/squish.glsl b/genlatte/genlatte-ui/widgetbook/assets/shaders/squish.glsl new file mode 120000 index 0000000..20b8839 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/assets/shaders/squish.glsl @@ -0,0 +1 @@ +../../../assets/shaders/squish.glsl \ No newline at end of file diff --git a/genlatte/genlatte-ui/widgetbook/assets/videos/CONTEXT.md b/genlatte/genlatte-ui/widgetbook/assets/videos/CONTEXT.md new file mode 100644 index 0000000..3044adc --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/assets/videos/CONTEXT.md @@ -0,0 +1,10 @@ +# Widgetbook Video Assets + +**Purpose:** +Mirrors the application's video assets for use within the Widgetbook component previews. + +**Detailed File Overviews:** +- `Dash_Loading_1080px.webm`: Used to preview the `LoadingDash` widget in Widgetbook. + +**Dependencies/Relationships:** +- Referenced by `genlatte-ui/widgetbook/lib/loading_dash_preview.dart`. diff --git a/genlatte/genlatte-ui/widgetbook/assets/videos/Dash_Loading_1080px.webm b/genlatte/genlatte-ui/widgetbook/assets/videos/Dash_Loading_1080px.webm new file mode 100644 index 0000000..299b6da Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/assets/videos/Dash_Loading_1080px.webm differ diff --git a/genlatte/genlatte-ui/widgetbook/assets/videos/Dash_Loading_1080px_H.265.mov b/genlatte/genlatte-ui/widgetbook/assets/videos/Dash_Loading_1080px_H.265.mov new file mode 120000 index 0000000..c116e0d --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/assets/videos/Dash_Loading_1080px_H.265.mov @@ -0,0 +1 @@ +../../../assets/videos/Dash_loading_1080px_H.265.mov \ No newline at end of file diff --git a/genlatte/genlatte-ui/widgetbook/lib/CONTEXT.md b/genlatte/genlatte-ui/widgetbook/lib/CONTEXT.md new file mode 100644 index 0000000..6168890 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/CONTEXT.md @@ -0,0 +1,12 @@ +# Widgetbook Component Previews + +**Purpose:** +Defines individual stories and previews for UI components using the Widgetbook framework. + +**Detailed File Overviews:** +- `main.dart`: Entry point for the Widgetbook application. +- `*_preview.dart`: Individual preview files for various shared widgets (buttons, cards, sliders, etc.). +- `main.directories.g.dart`: Autogenerated directory mapping for Widgetbook's navigation sidebar. + +**Dependencies/Relationships:** +- Previews widgets imported from `genlatte-ui/lib/src/widgets/`. diff --git a/genlatte/genlatte-ui/widgetbook/lib/chevon_button_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/chevon_button_preview.dart new file mode 100644 index 0000000..e74c695 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/chevon_button_preview.dart @@ -0,0 +1,55 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/chevron_button.dart'; +import 'package:widgetbook/widgetbook.dart'; + +@widgetbook.UseCase(name: 'Vertical', type: ChevronButton) +Widget buildVerticalChevronButtonUseCase(BuildContext context) { + return Center( + child: ChevronButton.vertical( + scale: context.knobs.double.slider( + label: 'Scale', + initialValue: 1.0, + min: 0.1, + max: 2.0, + divisions: 19, + ), + builder: (context, scale, textColor) => WrappedText( + style: (context, theme) => theme.typography.h1.copyWith( + fontSize: theme.typography.h1.fontSize! * scale, + color: textColor, + ), + child: Text(context.knobs.string(label: 'Text', initialValue: 'Next')), + ), + onPressed: () {}, + ), + ); +} + +@widgetbook.UseCase(name: 'Flat', type: ChevronButton) +Widget buildFlatChevronButtonUseCase(BuildContext context) { + return Center( + child: ChevronButton.flat( + scale: context.knobs.double.slider( + label: 'Scale', + initialValue: 1.0, + min: 0.1, + max: 2.0, + divisions: 19, + ), + builder: (context, scale, textColor) => WrappedText( + style: (context, theme) => theme.typography.h2.copyWith( + fontSize: theme.typography.h2.fontSize! * scale, + color: textColor, + ), + child: Text(context.knobs.string(label: 'Text', initialValue: 'Next')), + ), + onPressed: () {}, + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/configuration_card_buttons_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/configuration_card_buttons_preview.dart new file mode 100644 index 0000000..d947ddd --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/configuration_card_buttons_preview.dart @@ -0,0 +1,50 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/configuration_card.dart'; +import 'package:widgetbook/widgetbook.dart'; + +@widgetbook.UseCase(name: 'Buttons', type: ConfigurationCard) +Widget buildButtonsConfigurationCardUseCase(BuildContext context) { + return Center( + child: SizedBox( + width: context.knobs.double.slider( + label: 'Width', + initialValue: 600, + min: 150, + max: 2048, + ), + height: context.knobs.double.slider( + label: 'Height', + initialValue: 600, + min: 12, + max: 1536, + ), + child: Center( + child: ConfigurationCard.buttons( + flavor: context.knobs.object.segmented( + label: 'Flavor', + initialOption: .wood, + labelBuilder: (Flavor flavor) => flavor.name, + options: Flavor.values, + ), + onSelected: (String value) {}, + question: MultipleChoiceQuestion( + id: 'abc', + body: context.knobs.string( + label: 'Question', + initialValue: 'What time of day is it?', + ), + acceptableAnswers: ['Morning', 'Afternoon', 'Evening', 'Night'], + selectedValue: 'Morning', + ), + ), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/configuration_card_slider_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/configuration_card_slider_preview.dart new file mode 100644 index 0000000..3c4d48d --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/configuration_card_slider_preview.dart @@ -0,0 +1,56 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:genlatte_data/models.dart' show ZeroToOneQuestion; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/configuration_card.dart'; +import 'package:widgetbook/widgetbook.dart'; + +@widgetbook.UseCase(name: 'Slider', type: ConfigurationCard) +Widget buildSliderConfigurationCardUseCase(BuildContext context) { + return Center( + child: SizedBox( + width: context.knobs.double.slider( + label: 'Width', + initialValue: 600, + min: 150, + max: 2048, + ), + height: context.knobs.double.slider( + label: 'Height', + initialValue: 600, + min: 12, + max: 1536, + ), + child: Center( + child: ConfigurationCard.slider( + flavor: context.knobs.object.segmented( + label: 'Flavor', + initialOption: .wood, + labelBuilder: (Flavor flavor) => flavor.name, + options: Flavor.values, + ), + onSelected: (double value) {}, + question: ZeroToOneQuestion( + id: 'abc', + body: context.knobs.string( + label: 'Question', + initialValue: 'What time of day is it?', + ), + minValueLabel: context.knobs.string( + label: 'Min Value Label', + initialValue: 'Morning', + ), + maxValueLabel: context.knobs.string( + label: 'Max Value Label', + initialValue: 'Evening', + ), + ), + ), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/configuration_card_text_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/configuration_card_text_preview.dart new file mode 100644 index 0000000..ea4a3a6 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/configuration_card_text_preview.dart @@ -0,0 +1,49 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:genlatte_data/models.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/configuration_card.dart'; +import 'package:widgetbook/widgetbook.dart'; + +@widgetbook.UseCase(name: 'Text', type: ConfigurationCard) +Widget buildTextConfigurationCardUseCase(BuildContext context) { + return Center( + child: SizedBox( + width: context.knobs.double.slider( + label: 'Width', + initialValue: 600, + min: 150, + max: 2048, + ), + height: context.knobs.double.slider( + label: 'Height', + initialValue: 600, + min: 12, + max: 1536, + ), + child: Center( + child: ConfigurationCard.text( + flavor: context.knobs.object.segmented( + label: 'Flavor', + initialOption: .wood, + labelBuilder: (Flavor flavor) => flavor.name, + options: Flavor.values, + ), + onSelected: (String value) {}, + question: TextQuestion( + id: 'abc', + body: context.knobs.string( + label: 'Question', + initialValue: 'What time of day is it?', + ), + helpText: 'Your name', + ), + ), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/footer_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/footer_preview.dart new file mode 100644 index 0000000..dab54fa --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/footer_preview.dart @@ -0,0 +1,33 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/footer.dart'; +import 'package:widgetbook/widgetbook.dart'; + +@widgetbook.UseCase(name: 'Footer', type: Footer) +Widget buildFooterUseCase(BuildContext context) { + return Center( + child: ColoredBox( + color: Colors.black, + child: SizedBox( + width: context.knobs.double.slider( + label: 'Width', + initialValue: Footer.defaultWidth, + min: 150, + max: 2048, + ), + height: context.knobs.double.slider( + label: 'Height', + initialValue: Footer.defaultHeight, + min: 12, + max: 1536, + ), + child: Footer(), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/genlatte_filled_button_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/genlatte_filled_button_preview.dart new file mode 100644 index 0000000..9e80ff5 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/genlatte_filled_button_preview.dart @@ -0,0 +1,43 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/buttons.dart'; + +@widgetbook.UseCase(name: 'Icon & Text', type: GenLatteFilledButton) +Widget buildGenLatteFilledButtonDarkUseCase(BuildContext context) { + return Center( + child: Container( + height: 200, + width: 200, + color: Colors.black, + child: Center( + child: GenLatteFilledButton( + icon: Icons.arrow_back, + label: 'Back', + onPressed: () {}, + ), + ), + ), + ); +} + +@widgetbook.UseCase(name: 'Icon Only', type: GenLatteFilledButton) +Widget buildGenLatteFilledButtonIconOnlyUseCase(BuildContext context) { + return Center( + child: Container( + height: 200, + width: 200, + color: Colors.black, + child: Center( + child: GenLatteFilledButton( + icon: Icons.arrow_back, + onPressed: () {}, + ), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/genlatte_outlined_button_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/genlatte_outlined_button_preview.dart new file mode 100644 index 0000000..9db04d2 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/genlatte_outlined_button_preview.dart @@ -0,0 +1,35 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/buttons.dart'; + +@widgetbook.UseCase(name: 'Dark', type: GenLatteOutlinedButton) +Widget buildGenLatteOutlinedButtonDarkUseCase(BuildContext context) { + return Center( + child: GenLatteOutlinedButton.dark( + label: 'Dark', + onPressed: () {}, + ), + ); +} + +@widgetbook.UseCase(name: 'Light', type: GenLatteOutlinedButton) +Widget buildGenLatteOutlinedButtonLightUseCase(BuildContext context) { + return Center( + child: Container( + height: 200, + width: 200, + color: Colors.black, + child: Center( + child: GenLatteOutlinedButton.light( + label: 'Light', + onPressed: () {}, + ), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/latte_image_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/latte_image_preview.dart new file mode 100644 index 0000000..8e366a2 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/latte_image_preview.dart @@ -0,0 +1,30 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/widgets.dart'; + +@widgetbook.UseCase(name: 'Latte Image', type: LatteImageWidget) +Widget buildLatteImageUseCase(BuildContext context) { + return GenLatteScaffold( + child: Center( + child: Padding( + padding: EdgeInsets.all(60), + child: LatteImageWidget( + imageUrl: + "https://storage.googleapis.com/gcdemos-26-int-dd-latteart.firebasestorage.app/latteImages%2F0E4db620KWrbK3K5k13t%2F0.png", + dimension: context.knobs.double.slider( + label: 'Dimension', + initialValue: 200, + min: 100, + max: 500, + ), + ), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/latte_order_card_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/latte_order_card_preview.dart new file mode 100644 index 0000000..0da2ea1 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/latte_order_card_preview.dart @@ -0,0 +1,126 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/role.dart'; +import 'package:genlatte/src/screens/barista/widgets/widgets.dart'; +import 'package:genlatte_data/models.dart'; + +@widgetbook.UseCase(name: 'Latte Order Card', type: LatteOrderCard) +Widget buildLatteOrderCardUseCase(BuildContext context) { + final width = context.knobs.double.slider( + label: 'Width', + initialValue: 230, + min: 150, + max: 400, + ); + + final height = context.knobs.double.slider( + label: 'Height', + initialValue: 420, + min: 300, + max: 600, + ); + + final role = context.knobs.object.dropdown( + label: 'Role', + options: [Role.barista, Role.moderator], + initialOption: Role.barista, + ); + + final status = context.knobs.object.dropdown( + label: 'Status', + options: LatteOrderStatus.values, + initialOption: LatteOrderStatus.inProgress, + ); + + final canClaimOrders = context.knobs.boolean( + label: 'Can Claim Orders', + initialValue: true, + ); + + final isClaimed = context.knobs.boolean( + label: 'Is Claimed', + initialValue: true, + ); + + final isClaimedByMe = context.knobs.boolean( + label: 'Is Claimed By Me', + initialValue: true, + ); + + final myBarista = const Barista( + id: 'barista-me', + username: 'craig', + persona: BaristaPersona.caucasianMale, + ); + + final otherBarista = const Barista( + id: 'barista-other', + username: 'other', + persona: BaristaPersona.asianFemale, + ); + + final activeBarista = role == Role.barista ? myBarista : null; + + final claimedBy = isClaimed + ? (isClaimedByMe ? myBarista : otherBarista) + : null; + + final latte = Latte( + order: const LatteOrder( + id: 'order-123', + name: 'BigDumbIdiot', + milk: 'Whole Milk', + sweetener: 'Sugar', + happyPlace: 'A quiet beach at sunset', + ), + metadata: LatteOrderMetadata( + id: 'order-123', + orderNumber: 42, + isNameApproved: true, + isHappyPlaceApproved: true, + isImageApproved: true, + imageUrl: + 'https://storage.googleapis.com/gcdemos-26-int-dd-latteart.firebasestorage.app/latteImages%2F0E4db620KWrbK3K5k13t%2F0.png', + status: status, + baristaId: claimedBy?.id, + orderSubmittedTime: DateTime.now().subtract(const Duration(minutes: 5)), + ), + ); + + return Center( + child: SizedBox( + height: height, + width: width, + child: role == Role.barista + ? LatteOrderCard.barista( + activeBarista: activeBarista, + canClaimOrders: canClaimOrders, + claimedBy: claimedBy, + latte: latte, + onApproveAll: (_) {}, + onRejectName: (_) {}, + onRejectImage: (_) {}, + onRejectBoth: (_) {}, + onClaimPressed: (_) {}, + onCompletePressed: (_) {}, + onReprintPressed: (_) {}, + role: role, + ) + : LatteOrderCard.moderator( + claimedBy: claimedBy, + latte: latte, + onApproveAll: (_) {}, + onRejectName: (_) {}, + onRejectImage: (_) {}, + onRejectBoth: (_) {}, + onCompletePressed: (_) {}, + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/loading_dash_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/loading_dash_preview.dart new file mode 100644 index 0000000..46dd127 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/loading_dash_preview.dart @@ -0,0 +1,28 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/widgets.dart'; + +@widgetbook.UseCase(name: 'Loading Dash', type: LoadingDash) +Widget buildLoadingDashUseCase(BuildContext context) { + return GenLatteScaffold( + child: Center( + child: Padding( + padding: EdgeInsets.all( + context.knobs.double.slider( + label: 'Padding', + initialValue: 20, + min: 20, + max: 320, + ), + ), + child: LoadingDash(), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/main.dart b/genlatte/genlatte-ui/widgetbook/lib/main.dart new file mode 100644 index 0000000..bd5868f --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/main.dart @@ -0,0 +1,38 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/screens/app/theme.dart'; + +// This file does not exist yet, +// it will be generated in the next step +import 'main.directories.g.dart'; + +void main() { + runApp(const WidgetbookApp()); +} + +@widgetbook.App() +class WidgetbookApp extends StatelessWidget { + const WidgetbookApp({super.key}); + + @override + Widget build(BuildContext context) { + return Widgetbook( + appBuilder: (context, child) { + return ShadcnApp( + debugShowCheckedModeBanner: false, + theme: getThemeForOrientation(context), + home: child, + ); + }, + // The [directories] variable does not exist yet, + // it will be generated in the next step + directories: directories, + ); + } +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/main.directories.g.dart b/genlatte/genlatte-ui/widgetbook/lib/main.directories.g.dart new file mode 100644 index 0000000..305e1d5 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/main.directories.g.dart @@ -0,0 +1,222 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// dart format width=80 +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_import, prefer_relative_imports, directives_ordering + +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AppGenerator +// ************************************************************************** + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:widgetbook/widgetbook.dart' as _widgetbook; +import 'package:widgetbook_workspace/chevon_button_preview.dart' + as _widgetbook_workspace_chevon_button_preview; +import 'package:widgetbook_workspace/configuration_card_buttons_preview.dart' + as _widgetbook_workspace_configuration_card_buttons_preview; +import 'package:widgetbook_workspace/configuration_card_slider_preview.dart' + as _widgetbook_workspace_configuration_card_slider_preview; +import 'package:widgetbook_workspace/configuration_card_text_preview.dart' + as _widgetbook_workspace_configuration_card_text_preview; +import 'package:widgetbook_workspace/footer_preview.dart' + as _widgetbook_workspace_footer_preview; +import 'package:widgetbook_workspace/genlatte_filled_button_preview.dart' + as _widgetbook_workspace_genlatte_filled_button_preview; +import 'package:widgetbook_workspace/genlatte_outlined_button_preview.dart' + as _widgetbook_workspace_genlatte_outlined_button_preview; +import 'package:widgetbook_workspace/latte_image_preview.dart' + as _widgetbook_workspace_latte_image_preview; +import 'package:widgetbook_workspace/latte_order_card_preview.dart' + as _widgetbook_workspace_latte_order_card_preview; +import 'package:widgetbook_workspace/loading_dash_preview.dart' + as _widgetbook_workspace_loading_dash_preview; +import 'package:widgetbook_workspace/outlined_chevron_button_preview.dart' + as _widgetbook_workspace_outlined_chevron_button_preview; +import 'package:widgetbook_workspace/responsive_chevron_button_preview.dart' + as _widgetbook_workspace_responsive_chevron_button_preview; +import 'package:widgetbook_workspace/segmented_progress_preview.dart' + as _widgetbook_workspace_segmented_progress_preview; +import 'package:widgetbook_workspace/squish_widget_preview.dart' + as _widgetbook_workspace_squish_widget_preview; + +final directories = <_widgetbook.WidgetbookNode>[ + _widgetbook.WidgetbookFolder( + name: 'screens', + children: [ + _widgetbook.WidgetbookFolder( + name: 'barista', + children: [ + _widgetbook.WidgetbookFolder( + name: 'widgets', + children: [ + _widgetbook.WidgetbookComponent( + name: 'LatteOrderCard', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Latte Order Card', + builder: _widgetbook_workspace_latte_order_card_preview + .buildLatteOrderCardUseCase, + ), + ], + ), + ], + ), + ], + ), + _widgetbook.WidgetbookFolder( + name: 'kiosk', + children: [ + _widgetbook.WidgetbookFolder( + name: 'widgets', + children: [ + _widgetbook.WidgetbookComponent( + name: 'SegmentedProgress', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Default', + builder: _widgetbook_workspace_segmented_progress_preview + .buildSegmentedProgressUseCase, + ), + ], + ), + ], + ), + ], + ), + _widgetbook.WidgetbookFolder( + name: 'recent_orders', + children: [ + _widgetbook.WidgetbookFolder( + name: 'widgets', + children: [ + _widgetbook.WidgetbookComponent( + name: 'SquishedWidget', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Icon & Text', + builder: _widgetbook_workspace_squish_widget_preview + .squishWidgetPreviewUseCase, + ), + ], + ), + ], + ), + ], + ), + ], + ), + _widgetbook.WidgetbookFolder( + name: 'widgets', + children: [ + _widgetbook.WidgetbookComponent( + name: 'ChevronButton', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Flat', + builder: _widgetbook_workspace_chevon_button_preview + .buildFlatChevronButtonUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'Outlined', + builder: _widgetbook_workspace_outlined_chevron_button_preview + .buildChevronButtonUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'Responsive', + builder: _widgetbook_workspace_responsive_chevron_button_preview + .buildChevronButtonUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'Vertical', + builder: _widgetbook_workspace_chevon_button_preview + .buildVerticalChevronButtonUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'ConfigurationCard', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Buttons', + builder: _widgetbook_workspace_configuration_card_buttons_preview + .buildButtonsConfigurationCardUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'Slider', + builder: _widgetbook_workspace_configuration_card_slider_preview + .buildSliderConfigurationCardUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'Text', + builder: _widgetbook_workspace_configuration_card_text_preview + .buildTextConfigurationCardUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'Footer', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Footer', + builder: _widgetbook_workspace_footer_preview.buildFooterUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'GenLatteFilledButton', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Icon & Text', + builder: _widgetbook_workspace_genlatte_filled_button_preview + .buildGenLatteFilledButtonDarkUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'Icon Only', + builder: _widgetbook_workspace_genlatte_filled_button_preview + .buildGenLatteFilledButtonIconOnlyUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'GenLatteOutlinedButton', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Dark', + builder: _widgetbook_workspace_genlatte_outlined_button_preview + .buildGenLatteOutlinedButtonDarkUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'Light', + builder: _widgetbook_workspace_genlatte_outlined_button_preview + .buildGenLatteOutlinedButtonLightUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'LatteImageWidget', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Latte Image', + builder: _widgetbook_workspace_latte_image_preview + .buildLatteImageUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'LoadingDash', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Loading Dash', + builder: _widgetbook_workspace_loading_dash_preview + .buildLoadingDashUseCase, + ), + ], + ), + ], + ), +]; diff --git a/genlatte/genlatte-ui/widgetbook/lib/outlined_chevron_button_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/outlined_chevron_button_preview.dart new file mode 100644 index 0000000..e6f4940 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/outlined_chevron_button_preview.dart @@ -0,0 +1,44 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:widgetbook/widgetbook.dart'; + +@widgetbook.UseCase( + name: 'Outlined', + type: ChevronButton, +) +Widget buildChevronButtonUseCase(BuildContext context) { + return Center( + child: ColoredBox( + color: Colors.black, + child: SizedBox( + width: context.knobs.double.slider( + label: 'Width', + initialValue: 300, + min: 150, + max: 2048, + ), + height: context.knobs.double.slider( + label: 'Height', + initialValue: 500, + min: 12, + max: 1536, + ), + child: LayoutProvider( + child: ResponsiveChevronButton( + onPressed: () {}, + color: Colors.transparent, + borderColor: const Color(0xFFFBBC04), + textColor: const Color(0xFFFBBC04), + text: context.knobs.string(label: 'Text', initialValue: 'Next'), + ), + ), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/responsive_chevron_button_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/responsive_chevron_button_preview.dart new file mode 100644 index 0000000..a161a77 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/responsive_chevron_button_preview.dart @@ -0,0 +1,41 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/widgets/widgets.dart'; +import 'package:widgetbook/widgetbook.dart'; + +@widgetbook.UseCase( + name: 'Responsive', + type: ChevronButton, +) +Widget buildChevronButtonUseCase(BuildContext context) { + return Center( + child: ColoredBox( + color: Colors.black, + child: SizedBox( + width: context.knobs.double.slider( + label: 'Width', + initialValue: 300, + min: 150, + max: 2048, + ), + height: context.knobs.double.slider( + label: 'Height', + initialValue: 500, + min: 12, + max: 1536, + ), + child: LayoutProvider( + child: ResponsiveChevronButton( + onPressed: () {}, + text: context.knobs.string(label: 'Text', initialValue: 'Next'), + ), + ), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/segmented_progress_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/segmented_progress_preview.dart new file mode 100644 index 0000000..a980b14 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/segmented_progress_preview.dart @@ -0,0 +1,25 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'package:genlatte/src/screens/kiosk/widgets/segmented_progress.dart'; +import 'package:widgetbook/widgetbook.dart'; + +@widgetbook.UseCase(name: 'Default', type: SegmentedProgress) +Widget buildSegmentedProgressUseCase(BuildContext context) { + return Center( + child: SegmentedProgress( + currentStep: context.knobs.int.input( + label: 'Current Step', + initialValue: 0, + ), + totalSteps: context.knobs.int.input( + label: 'Total Steps', + initialValue: 4, + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/lib/squish_widget_preview.dart b/genlatte/genlatte-ui/widgetbook/lib/squish_widget_preview.dart new file mode 100644 index 0000000..dd1cdfb --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/lib/squish_widget_preview.dart @@ -0,0 +1,50 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' show pi; + +import 'package:flutter/material.dart'; +import 'package:genlatte/src/screens/recent_orders/widgets/widgets.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; +import 'package:widgetbook/widgetbook.dart'; + +@widgetbook.UseCase(name: 'Icon & Text', type: SquishedWidget) +Widget squishWidgetPreviewUseCase(BuildContext context) { + return Center( + child: SquishedWidget( + direction: context.knobs.double.slider( + label: 'Direction', + initialValue: 0, + min: -pi, + max: pi, + ), + effectStrength: context.knobs.double.slider( + label: 'Effect Strength', + initialValue: 0, + min: -1, + max: 10, + ), + child: SizedBox( + width: 200, + height: 200, + child: Stack( + children: [ + Positioned( + left: 50, + top: 50, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Colors.red, + ), + ), + ), + ], + ), + ), + ), + ); +} diff --git a/genlatte/genlatte-ui/widgetbook/pubspec.yaml b/genlatte/genlatte-ui/widgetbook/pubspec.yaml new file mode 100644 index 0000000..4774723 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/pubspec.yaml @@ -0,0 +1,45 @@ +name: widgetbook_workspace +description: GenLatte widget catalog / browser. +publish_to: 'none' +version: 0.1.0+1 + +environment: + sdk: ^3.10.0 + +dependencies: + data_layer: 0.0.5 + data_layer_hive: ^0.0.2-rc.1 + flutter: + sdk: flutter + flutter_animate: ^4.5.2 + flutter_shaders: ^0.1.3 + genlatte: + path: ../ + genlatte_data: + path: ../../genlatte-data + shadcn_flutter: ^0.0.52 + video_player: ^2.11.1 + widgetbook: ^3.20.2 + widgetbook_annotation: ^3.9.0 + +dev_dependencies: + build_runner: ^2.10.5 + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + widgetbook_generator: ^3.20.1 + +flutter: + uses-material-design: true + + assets: + - assets/firebase-logo.png + - assets/flutter-logo.png + - assets/gemini-logo.png + - assets/latte-background.png + + - assets/videos/Dash_Loading_1080px.webm + - assets/videos/Dash_Loading_1080px_H.265.mov + + shaders: + - assets/shaders/squish.glsl diff --git a/genlatte/genlatte-ui/widgetbook/web/CONTEXT.md b/genlatte/genlatte-ui/widgetbook/web/CONTEXT.md new file mode 100644 index 0000000..8be67cb --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/web/CONTEXT.md @@ -0,0 +1,14 @@ +# Widgetbook Web Entry + +**Purpose:** +Coordinates the web-based delivery of the Widgetbook component library. + +**Detailed File Overviews:** +- `index.html`: Web entry point. +- `manifest.json`: Web manifest for PWA support. + +**Subdirectories:** +- `icons/`: PWA icon assets. + +**Dependencies/Relationships:** +- Serves the `genlatte-ui` component previews to a browser. diff --git a/genlatte/genlatte-ui/widgetbook/web/favicon.png b/genlatte/genlatte-ui/widgetbook/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/web/favicon.png differ diff --git a/genlatte/genlatte-ui/widgetbook/web/icons/CONTEXT.md b/genlatte/genlatte-ui/widgetbook/web/icons/CONTEXT.md new file mode 100644 index 0000000..836a9b3 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/web/icons/CONTEXT.md @@ -0,0 +1,10 @@ +# Widgetbook Web Icons + +**Purpose:** +Provides icon assets specific to the Widgetbook (Storybook) web build. + +**Detailed File Overviews:** +- `Icon-192.png` / `Icon-512.png`: Standard and maskable icons for the Widgetbook PWA. + +**Dependencies/Relationships:** +- Used by the `genlatte-ui/widgetbook/web` entry point. diff --git a/genlatte/genlatte-ui/widgetbook/web/icons/Icon-192.png b/genlatte/genlatte-ui/widgetbook/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/web/icons/Icon-192.png differ diff --git a/genlatte/genlatte-ui/widgetbook/web/icons/Icon-512.png b/genlatte/genlatte-ui/widgetbook/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/web/icons/Icon-512.png differ diff --git a/genlatte/genlatte-ui/widgetbook/web/icons/Icon-maskable-192.png b/genlatte/genlatte-ui/widgetbook/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/web/icons/Icon-maskable-192.png differ diff --git a/genlatte/genlatte-ui/widgetbook/web/icons/Icon-maskable-512.png b/genlatte/genlatte-ui/widgetbook/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/genlatte/genlatte-ui/widgetbook/web/icons/Icon-maskable-512.png differ diff --git a/genlatte/genlatte-ui/widgetbook/web/index.html b/genlatte/genlatte-ui/widgetbook/web/index.html new file mode 100644 index 0000000..2e6b855 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + widgetbook + + + + + + + diff --git a/genlatte/genlatte-ui/widgetbook/web/manifest.json b/genlatte/genlatte-ui/widgetbook/web/manifest.json new file mode 100644 index 0000000..6bd71b6 --- /dev/null +++ b/genlatte/genlatte-ui/widgetbook/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "widgetbook", + "short_name": "widgetbook", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/genui_workshop/.gitignore b/genui_workshop/.gitignore new file mode 100644 index 0000000..475d165 --- /dev/null +++ b/genui_workshop/.gitignore @@ -0,0 +1,32 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock + +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ + +# dotenv environment variables file +.env* + +# Avoid committing generated Javascript files: +*.dart.js +# Produced by the --dump-info flag. +*.info.json +# When generated by dart2js. Don't specify *.js if your +# project includes source files written in JavaScript. +*.js +*.js_ +*.js.deps +*.js.map + +.flutter-plugins +.flutter-plugins-dependencies +firebase.json +lib/firebase_options.dart +.DS_Store diff --git a/genui_workshop/.metadata b/genui_workshop/.metadata new file mode 100644 index 0000000..9bf27ae --- /dev/null +++ b/genui_workshop/.metadata @@ -0,0 +1,45 @@ +# 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: "437ec2cbd40a0a28b6f7528e49985ed14580ff0e" + channel: "master" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + base_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + - platform: android + create_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + base_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + - platform: ios + create_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + base_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + - platform: linux + create_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + base_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + - platform: macos + create_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + base_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + - platform: web + create_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + base_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + - platform: windows + create_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + base_revision: 437ec2cbd40a0a28b6f7528e49985ed14580ff0e + + # 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' diff --git a/genui_workshop/README.md b/genui_workshop/README.md new file mode 100644 index 0000000..666824b --- /dev/null +++ b/genui_workshop/README.md @@ -0,0 +1,871 @@ +# genui_workshop + +## Step 1: Set up Firebase + +### Create a Firebase project +1. Sign into the [Firebase console](https://console.firebase.google.com/) + using your Google Account. + +1. Click the button to create a new project, and then enter a project name + (for example, `GenUI Workshop`).
+ +1. Click **Continue**. + +1. If prompted, review and accept the Firebase terms, and then click + **Continue**. + +1. Click **Create project**, wait for your project to provision, and then click + **Continue**. + +### Enable Firebase AI Logic + +To enable Firebase AI Logic in your Firebase project: + +1. In the Firebase console, go to **AI Services** > + [**AI Logic**](https://console.firebase.google.com/project/_/ailogic/). + +1. Click **Get started**. + +1. Choose to use the _Gemini Developer API_ by clicking + **Get started with this API**. + +1. Click **Enable API** and confirm. + +## Step 2: Activate Cloud Shell + +Cloud Shell space is persistent between projects so make sure you are creating things in proper project directories. + +## Step 3: Creating the Flutter project + +Cloud shell needs to link to the stored version of Flutter so we need the following command before running `flutter`. + +```shell +git config --global --add safe.directory /google/flutter +``` + +We'll be starting with an empty project and adding code as we go. Warning that this might take a little while on Cloud Shell. + +```shell +flutter create --empty genui_workshop +``` + +Next we can attach our local code to the previously created project with `flutterfire`. We only need the web target. + +```shell +dart pub global activate flutterfire_cli +export PATH="$PATH":"$HOME/.pub-cache/bin" + +cd genui_workshop +flutterfire configure +``` +Select the project you just created; +deselect all but web + +```shell +flutter pub add genui firebase_core firebase_ai json_schema_builder +``` + +Let's run the app. + +Cloud shell doesn't allow `flutter run -d chrome` by default so we will test it by building the web target and starting a server. +```shell +flutter build web +python -m http.server 8080 --directory build/web +``` + +## Step 4: Setup GenUI scaffolding. + +Create `lib/genui_utils.dart` +```dart +sealed class ConversationItem {} + +class TextItem extends ConversationItem { + final String text; + final bool isUser; + TextItem({required this.text, this.isUser = false}); +} +``` + +Create `lib/message_bubble.dart`. +```dart +import 'package:flutter/material.dart'; + +class MessageBubble extends StatelessWidget { + final String text; + final bool isUser; + + const MessageBubble({super.key, required this.text, required this.isUser}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + final bubbleColor = isUser + ? colorScheme.primary + : colorScheme.surfaceContainerHighest; + + final textColor = isUser + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 8.0), + child: Column( + crossAxisAlignment: isUser + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: isUser + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + decoration: BoxDecoration( + color: bubbleColor, + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(20), + topRight: const Radius.circular(20), + bottomLeft: Radius.circular(isUser ? 20 : 0), + bottomRight: Radius.circular(isUser ? 0 : 20), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(20), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + gradient: isUser + ? LinearGradient( + colors: [ + colorScheme.primary, + colorScheme.primary.withAlpha(200), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + ), + child: Text( + text, + style: theme.textTheme.bodyLarge?.copyWith( + color: textColor, + height: 1.3, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 2), + ], + ), + ); + } +} + +``` + +Paste the following to `main.dart` +```dart +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:genui_workshop/message_bubble.dart'; +import 'package:genui_workshop/genui_utils.dart'; +import 'firebase_options.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Weather Today', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final List _items = []; + final _textController = TextEditingController(); + final _scrollController = ScrollController(); + late final ChatSession _chatSession; + + @override + void initState() { + super.initState(); + final model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-3.1-flash-lite', + ); + _chatSession = model.startChat(); + _chatSession.sendMessage(Content.text(systemInstruction)); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + @override + void dispose() { + _textController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + Future _addMessage() async { + final text = _textController.text; + + if (text.trim().isEmpty) { + return; + } + _textController.clear(); + + setState(() { + _items.add(TextItem(text: text, isUser: true)); + }); + + _scrollToBottom(); + + final response = await _chatSession.sendMessage(Content.text(text)); + + if (response.text?.isNotEmpty ?? false) { + setState(() { + _items.add(TextItem(text: response.text!, isUser: false)); + }); + _scrollToBottom(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Weather'), + ), + body: Column( + children: [ + Expanded( + child: ListView( + controller: _scrollController, + padding: const EdgeInsets.all(16), + children: [ + for (final item in _items) + switch (item) { + TextItem() => MessageBubble( + text: item.text, + isUser: item.isUser, + ), + }, + ], + ), + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _textController, + onSubmitted: (_) => _addMessage(), + decoration: const InputDecoration( + hintText: 'Enter a message', + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _addMessage, + child: const Text('Send'), + ), + ], + ), + ), + ), + ], + ), + ); + } +} +``` + + +Add the system instruction to `genui_utils.dart`. +```dart + const systemInstruction = ''' + ## PERSONA + You are a meteorologist. + + ## GOAL + Work with me to produce of weather forecasts. + + ## RULES + + Do not offer opinions unless I ask for them. + + ## PROCESS + ### Planning + * Ask me for a location to check the weather. + * Follow up and ask for a date if not provided. + * Synthesize a list of weather forecasts from the provided information. + * Where available, you will use tool calls to retreive the info (not implemented yet) + * Advise if you are pulling the data from a real source or making it up. + * Ask clarifying questions if you need to. + * Respond to my suggestions for changes to date or location, if I have any. +'''; +``` + +Run the app. +It runs and responds with weather information, but no generative UI yet. + +## Integrate GenUI package +Add the following to `genui_utils.dart` +```dart +class SurfaceItem extends ConversationItem { + final String surfaceId; + SurfaceItem({required this.surfaceId}); +} + +``` + +In `main.dart`, add a case for the `SurfaceItem` type to the ListView `build` method: +```dart +Expanded( + child: ListView( + controller: _scrollController, + padding: const EdgeInsets.all(16), + children: [ + for (final item in _items) + switch (item) { + TextItem() => MessageBubble( + text: item.text, + isUser: item.isUser, + ), + // New! + SurfaceItem() => Surface( + surfaceContext: _controller.contextFor( + item.surfaceId, + ), + ), + }, + ], + ), +), +``` + + +At the top of lib/main.dart, import the genui library: +```dart +import 'package:genui/genui.dart' hide TextPart; +import 'package:genui/genui.dart' as genui; +``` + + +Add the following lifecycle controllers and adapters to `main.dart` +```dart +class _MyHomePageState extends State { + // ... existing members + late final ChatSession _chatSession; + + // Add GenUI controllers + late final SurfaceController _controller; + late final A2uiTransportAdapter _transport; + late final Conversation _conversation; + late final Catalog catalog; +``` + +Run the app. + +Update `_addMessage` and `sendAndReceive` like in code lab. + +```dart + Future _addMessage() async { + final text = _textController.text; + + if (text.trim().isEmpty) { + return; + } + + _textController.clear(); + + setState(() { + _items.add(TextItem(text: text, isUser: true)); + }); + + _scrollToBottom(); + + // Send the user's input through GenUI instead of directly to Firebase. + await _conversation.sendRequest(ChatMessage.user(text)); + } +``` + +```dart + Future _sendAndReceive(ChatMessage msg) async { + final buffer = StringBuffer(); + + // Reconstruct the message part fragments + for (final part in msg.parts) { + if (part.isUiInteractionPart) { + buffer.write(part.asUiInteractionPart!.interaction); + print(part.asUiInteractionPart!.interaction); + } else if (part is genui.TextPart) { + buffer.write(part.text); + } + } + + if (buffer.isEmpty) { + return; + } + + final text = buffer.toString(); + + // Send the string to Firebase AI Logic. + final response = await _chatSession.sendMessage(Content.text(text)); + + if (response.text?.isNotEmpty ?? false) { + // Feed the response back into GenUI's transportation layer + _transport.addChunk(response.text!); + } + } +``` + +### Wire up A2UI + +Add to the end of initState. +```dart + // remove this line + // _chatSession.sendMessage(Content.text(systemInstruction)); + + // Initialize the GenUI Catalog. + // The genui package provides a default set of primitive widgets (like text + // and basic buttons) out of the box using this class. + catalog = BasicCatalogItems.asCatalog().copyWith( +// newItems: [weatherInput, weatherCard], + ); + + // Create a SurfaceController to manage the state of generated surfaces. + _controller = SurfaceController(catalogs: [catalog]); + + // Create a transport adapter that will process messages to and from the + // agent, looking for A2UI messages. + _transport = A2uiTransportAdapter(onSend: _sendAndReceive); + + // Link the transport and SurfaceController together in a Conversation, + // which provides your app a unified API for interacting with the agent. + _conversation = Conversation( + controller: _controller, + transport: _transport, + ); + + // Listen to GenUI stream events to update the UI + _conversation.events.listen((event) { + setState(() { + switch (event) { + case ConversationSurfaceAdded added: + _items.add(SurfaceItem(surfaceId: added.surfaceId)); + _scrollToBottom(); + case ConversationSurfaceRemoved removed: + _items.removeWhere( + (item) => + item is SurfaceItem && item.surfaceId == removed.surfaceId, + ); + case ConversationContentReceived content: + _items.add(TextItem(text: content.text, isUser: false)); + _scrollToBottom(); + case ConversationError error: + debugPrint('GenUI Error: ${error.error}'); + default: + } + }); + }); + + // Create the system prompt for the agent, which will include this app's + // system instruction as well as the schema for the catalog. + final promptBuilder = PromptBuilder.chat( + catalog: catalog, + systemPromptFragments: [systemInstruction], + ); + + // Send the prompt into the Conversation, which will subsequently route it + // to Firebase using the transport mechanism. + _conversation.sendRequest( + ChatMessage.system(promptBuilder.systemPromptJoined()), + ); +``` + +We could explore doing some progress indicator UI here or not. + +## Make weather input widget +The components it picks is random and can't be submitted sometimes + +Create a `lib/catalog` directory. One file per item with the schema included. + +Add the following to `lib/catalog/weather_input.dart` +```dart +import 'package:flutter/material.dart'; +import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +final simpleWeatherSchema = S.object( + properties: { + 'location': S.string(description: 'The location to check the weather.'), + 'date': S.string(description: 'The date to check the weather.'), + }, +); + +// Example raw action +//{version: v0.9, action: {name: submit_weather_request, sourceComponentId: submitButton, +//timestamp: 2026-05-26T22:10:19.087, context: {}, surfaceId: weather_input_surface}} +final weatherInput = CatalogItem( + name: 'WeatherInput', + dataSchema: simpleWeatherSchema, + widgetBuilder: (itemContext) { + final json = itemContext.data as Map; + final data = SimpleWeatherData.fromJson(json); + return WeatherInput( + data: data, + onFetchRequest: (loc, date) async { + final JsonMap resolvedContext = await resolveContext( + itemContext.dataContext, + {'location': loc, 'date': date.toString()}, + ); + itemContext.dispatchEvent( + UserActionEvent( + name: 'submit_weather_request', + sourceComponentId: 'submitButton', + timestamp: DateTime.now(), + context: resolvedContext + ), + ); + }, + ); + }, +); + +class SimpleWeatherData { + String location; + DateTime date; + + SimpleWeatherData({required this.location, required this.date}); + + factory SimpleWeatherData.defaultValues() { + return SimpleWeatherData(location: 'Baltimore', date: DateTime.now()); + } + + factory SimpleWeatherData.fromJson(Map json) { + if (json.isNotEmpty) { + return SimpleWeatherData( + location: json['location'] as String, + date: DateTime.parse(json['date'] as String), + ); + } else { + + return SimpleWeatherData(location: 'Baltimore', date: DateTime.now()); + } + } +} + +class WeatherInput extends StatefulWidget { + final SimpleWeatherData data; + final void Function(String, DateTime) onFetchRequest; + + const WeatherInput({ + super.key, + required this.data, + required this.onFetchRequest, + }); + + @override + State createState() => _WeatherInputState(); +} + +class _WeatherInputState extends State { + late TextEditingController _controller; + DateTime? selectedDate = DateTime.now(); + + Future _selectDate() async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now().copyWith(month: 1, day: 1), + lastDate: DateTime.now().copyWith(month: 12, day: 31), + ); + + setState(() { + selectedDate = pickedDate; + }); + } + + @override + void initState() { + // TODO: implement initState + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + + @override + Widget build(BuildContext context) { + return Container( + width: 320, + padding: const EdgeInsets.all(16), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Location", style: TextStyle(fontWeight: FontWeight.bold) ), + const SizedBox(height: 8), + TextField(decoration: InputDecoration(border: OutlineInputBorder()), controller: _controller,), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 50, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), + ), + onPressed: () { + + widget.onFetchRequest(_controller.text, selectedDate!); + }, + child: const Text("Get Forecast", style: TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ), + ), + ); + } +} +``` + +Add `weatherInput` as a new item in the basic catalog by importing it and adding it to `newItems` in `lib/main.dart`: + +```dart +import 'package:genui_workshop/catalog/weather_input.dart'; +// ... + catalog = BasicCatalogItems.asCatalog().copyWith( + newItems: [weatherInput], + ); +``` + +Change the system prompt to point it to the catalog item. +Add this to end of the system instruction prompt: + +```dart + + ## USER INTERFACE + * To request the location to retreive weather, create an instance of the WeatherInput + catalog item. +``` + +Create a file called `lib/catalog/weather_card.dart` + +```dart +import 'package:flutter/material.dart'; +import 'package:genui_workshop/genui_utils.dart'; +import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'dart:io'; + +import 'dart:convert'; +import 'fake_forecast.dart'; + +final forecastSchema = S.object( + properties: { + 'area_name': S.string(), + //"flags": S.object(), + 'current_condition': S.object( + properties: { + "temp_C": S.number(), + "temp_F": S.number(), + "humidity": S.number(), + "observation_time": S.string(), + }, + ), + 'weatherDesc': S.string(), + 'weatherIconUrl': S.string(), + }, +); + +final weatherCard = CatalogItem( + name: 'WeatherCard', + dataSchema: forecastSchema, + widgetBuilder: (itemContext) { + return WeatherCard(data: itemContext.data as Map); + }, +); + +class WeatherCard extends StatelessWidget { + final Map data; + const WeatherCard({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + final currentCondition = data["current_condition"] as Map; + return Container( + width: 500, + padding: const EdgeInsets.all(20), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: .end, + children: [ + Text( + "Observed at ${currentCondition["observation_time"]}", + style: TextStyle(fontSize: 12), + ), + ], + ), + const SizedBox(height: 8), + // Header: Icon and City Name + Row( + children: [ + Image.network( + data["weatherIconUrl"].toString(), + errorBuilder: (context, err, trace) { + return Icon(Icons.wb_sunny, size: 32,); + }, + ), + const SizedBox(width: 16), + Text( + data["area_name"].toString(), + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 24), + + // Temperature Display + Row( + children: [ + Text( + currentCondition["temp_C"]!.toString(), + style: TextStyle(fontSize: 56, fontWeight: FontWeight.bold), + ), + SizedBox(width: 8), + Text( + "°C", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.w500), + ), + ], + ), + const SizedBox(height: 16), + + // Low Temp row + Row( + children: [ + const SizedBox(width: 16), + Text( + currentCondition['temp_F'].toString(), + style: TextStyle(fontSize: 28, fontWeight: FontWeight.w500), + ), + SizedBox(width: 8), + Text( + "°F", + style: TextStyle(fontSize: 20, color: Colors.grey), + ), + ], + ), + const SizedBox(height: 16), + + // Weather Condition + Text("${data['weatherDesc']}", style: TextStyle(fontSize: 22)), + ], + ), + ), + ), + ); + } +} +``` + +Add the `weatherCard` item to your catalog inside `lib/main.dart`: + +```dart +import 'package:genui_workshop/catalog/weather_card.dart'; +// ... + catalog = BasicCatalogItems.asCatalog().copyWith( + newItems: [weatherInput, weatherCard], + ); +``` + +Create a file called `lib/catalog/fake_forecast.dart` +```dart +var fakeForecast = { + 'area_name': 'Baltimore (Mount Clare)', + 'current_condition': { + 'temp_C': 22, + 'temp_F': 72, + 'humidity': 61, + 'observation_time': '02:25 PM', + }, + 'weatherDesc': ' Sunny', + 'weatherIconUrl': + 'https://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0001_sunny.png', +}; +``` + +Run the app. + +## Learn more + +For more information and detailed references on the technologies used in this workshop, check out the following resources: + +- **Flutter**: Discover guides, tutorials, and widget references at the [Official Flutter Documentation](https://docs.flutter.dev). +- **Firebase AI Logic**: Learn how to securely integrate generative AI models directly into your web and mobile apps on the [Firebase AI Logic Documentation](https://firebase.google.com/docs/ai-logic). +- **A2UI Protocol**: Understand the Agent-to-User Interface protocol specification and ecosystem at the [A2UI Official Site](https://a2ui.org). +- [**GenUI SDK for Flutter**](https://docs.flutter.dev/ai/genui) diff --git a/genui_workshop/analysis_options.yaml b/genui_workshop/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/genui_workshop/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/genui_workshop/flutter_run_cloudshell.sh b/genui_workshop/flutter_run_cloudshell.sh new file mode 100755 index 0000000..255435f --- /dev/null +++ b/genui_workshop/flutter_run_cloudshell.sh @@ -0,0 +1,3 @@ +#!/bin/sh +flutter build web +python -m http.server 8080 --directory build/web diff --git a/genui_workshop/lib/catalog/fake_forecast.dart b/genui_workshop/lib/catalog/fake_forecast.dart new file mode 100644 index 0000000..e2c5b77 --- /dev/null +++ b/genui_workshop/lib/catalog/fake_forecast.dart @@ -0,0 +1,12 @@ +var fakeForecast = { + 'area_name': 'Baltimore (Mount Clare)', + 'current_condition': { + 'temp_C': 22, + 'temp_F': 72, + 'humidity': 61, + 'observation_time': '02:25 PM', + }, + 'weatherDesc': ' Sunny', + 'weatherIconUrl': + 'https://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0001_sunny.png', +}; \ No newline at end of file diff --git a/genui_workshop/lib/catalog/weather_card.dart b/genui_workshop/lib/catalog/weather_card.dart new file mode 100644 index 0000000..4f369ef --- /dev/null +++ b/genui_workshop/lib/catalog/weather_card.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:genui_workshop/genui_utils.dart'; +import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'dart:io'; + +import 'dart:convert'; +import 'fake_forecast.dart'; + +final forecastSchema = S.object( + properties: { + 'area_name': S.string(), + //"flags": S.object(), + 'current_condition': S.object( + properties: { + "temp_C": S.number(), + "temp_F": S.number(), + "humidity": S.number(), + "observation_time": S.string(), + }, + ), + 'weatherDesc': S.string(), + 'weatherIconUrl': S.string(), + }, +); + +final weatherCard = CatalogItem( + name: 'WeatherCard', + dataSchema: forecastSchema, + widgetBuilder: (itemContext) { + return WeatherCard(data: itemContext.data as Map); + }, +); + +class WeatherCard extends StatelessWidget { + final Map data; + const WeatherCard({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + final currentCondition = data["current_condition"] as Map; + return Container( + width: 500, + padding: const EdgeInsets.all(20), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: .end, + children: [ + Text( + "Observed at ${currentCondition["observation_time"]}", + style: TextStyle(fontSize: 12), + ), + ], + ), + const SizedBox(height: 8), + // Header: Icon and City Name + Row( + children: [ + Image.network( + data["weatherIconUrl"].toString(), + errorBuilder: (context, err, trace) { + return Icon(Icons.wb_sunny, size: 32,); + }, + ), + const SizedBox(width: 16), + Text( + data["area_name"].toString(), + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 24), + + // Temperature Display + Row( + children: [ + Text( + currentCondition["temp_C"]!.toString(), + style: TextStyle(fontSize: 56, fontWeight: FontWeight.bold), + ), + SizedBox(width: 8), + Text( + "°C", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.w500), + ), + ], + ), + const SizedBox(height: 16), + + // Low Temp row + Row( + children: [ + const SizedBox(width: 16), + Text( + currentCondition['temp_F'].toString(), + style: TextStyle(fontSize: 28, fontWeight: FontWeight.w500), + ), + SizedBox(width: 8), + Text( + "°F", + style: TextStyle(fontSize: 20, color: Colors.grey), + ), + ], + ), + const SizedBox(height: 16), + + // Weather Condition + Text("${data['weatherDesc']}", style: TextStyle(fontSize: 22)), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/genui_workshop/lib/catalog/weather_input.dart b/genui_workshop/lib/catalog/weather_input.dart new file mode 100644 index 0000000..8df9eee --- /dev/null +++ b/genui_workshop/lib/catalog/weather_input.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +final simpleWeatherSchema = S.object( + properties: { + 'location': S.string(description: 'The location to check the weather.'), + 'date': S.string(description: 'The date to check the weather.'), + }, +); + +// Example raw action +//{version: v0.9, action: {name: submit_weather_request, sourceComponentId: submitButton, +//timestamp: 2026-05-26T22:10:19.087, context: {}, surfaceId: weather_input_surface}} +final weatherInput = CatalogItem( + name: 'WeatherInput', + dataSchema: simpleWeatherSchema, + widgetBuilder: (itemContext) { + final json = itemContext.data as Map; + final data = SimpleWeatherData.fromJson(json); + return WeatherInput( + data: data, + onFetchRequest: (loc, date) async { + final JsonMap resolvedContext = await resolveContext( + itemContext.dataContext, + {'location': loc, 'date': date.toString()}, + ); + itemContext.dispatchEvent( + UserActionEvent( + name: 'submit_weather_request', + sourceComponentId: 'submitButton', + timestamp: DateTime.now(), + context: resolvedContext + ), + ); + }, + ); + }, +); + +class SimpleWeatherData { + String location; + DateTime date; + + SimpleWeatherData({required this.location, required this.date}); + + factory SimpleWeatherData.defaultValues() { + return SimpleWeatherData(location: 'Baltimore', date: DateTime.now()); + } + + factory SimpleWeatherData.fromJson(Map json) { + if (json.isNotEmpty) { + return SimpleWeatherData( + location: json['location'] as String, + date: DateTime.parse(json['date'] as String), + ); + } else { + + return SimpleWeatherData(location: 'Baltimore', date: DateTime.now()); + } + } +} + +class WeatherInput extends StatefulWidget { + final SimpleWeatherData data; + final void Function(String, DateTime) onFetchRequest; + + const WeatherInput({ + super.key, + required this.data, + required this.onFetchRequest, + }); + + @override + State createState() => _WeatherInputState(); +} + +class _WeatherInputState extends State { + late TextEditingController _controller; + DateTime? selectedDate = DateTime.now(); + + Future _selectDate() async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now().copyWith(month: 1, day: 1), + lastDate: DateTime.now().copyWith(month: 12, day: 31), + ); + + setState(() { + selectedDate = pickedDate; + }); + } + + @override + void initState() { + // TODO: implement initState + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + + @override + Widget build(BuildContext context) { + return Container( + width: 320, + padding: const EdgeInsets.all(16), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Location", style: TextStyle(fontWeight: FontWeight.bold) ), + const SizedBox(height: 8), + TextField(decoration: InputDecoration(border: OutlineInputBorder()), controller: _controller,), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 50, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), + ), + onPressed: () { + + widget.onFetchRequest(_controller.text, selectedDate!); + }, + child: const Text("Get Forecast", style: TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/genui_workshop/lib/genui_utils.dart b/genui_workshop/lib/genui_utils.dart new file mode 100644 index 0000000..5da06a3 --- /dev/null +++ b/genui_workshop/lib/genui_utils.dart @@ -0,0 +1,38 @@ +sealed class ConversationItem {} + +class TextItem extends ConversationItem { + final String text; + final bool isUser; + TextItem({required this.text, this.isUser = false}); +} + +class SurfaceItem extends ConversationItem { + final String surfaceId; + SurfaceItem({required this.surfaceId}); +} + +const systemInstruction = ''' + ## PERSONA + You are a meteorologist. + + ## GOAL + Work with me to produce of weather forecasts. + + ## RULES + + Do not offer opinions unless I ask for them. + + ## PROCESS + ### Planning + * Ask me for a location to check the weather. + * Follow up and ask for a date if not provided. + * Synthesize a list of weather forecasts from the provided information. + * Where available, you will use tool calls to retreive the info (not implemented yet) + * Advise if you are pulling the data from a real source or making it up. + * Ask clarifying questions if you need to. + * Respond to my suggestions for changes to date or location, if I have any. + + ## USER INTERFACE + * To request the location to retreive weather, create an instance of the WeatherInput + catalog item. +'''; \ No newline at end of file diff --git a/genui_workshop/lib/main.dart b/genui_workshop/lib/main.dart new file mode 100644 index 0000000..6e11973 --- /dev/null +++ b/genui_workshop/lib/main.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:genui_workshop/catalog/weather_card.dart'; +import 'package:genui_workshop/catalog/weather_input.dart'; +import 'package:genui_workshop/message_bubble.dart'; +import 'package:genui_workshop/genui_utils.dart'; +import 'firebase_options.dart'; + +import 'package:genui/genui.dart' hide TextPart; +import 'package:genui/genui.dart' as genui; +import 'package:genui_workshop/tool_calls.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Weather Today', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final List _items = []; + final _textController = TextEditingController(); + final _scrollController = ScrollController(); + late final ChatSession _chatSession; + + // Add GenUI controllers + late final SurfaceController _controller; + late final A2uiTransportAdapter _transport; + late final Conversation _conversation; + late final Catalog catalog; + + @override + void initState() { + super.initState(); + final model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-3.1-flash-lite', + ); + + _chatSession = model.startChat(); + + // Initialize the GenUI Catalog. + // The genui package provides a default set of primitive widgets (like text + // and basic buttons) out of the box using this class. + catalog = BasicCatalogItems.asCatalog().copyWith( + newItems: [weatherInput,weatherCard ], + ); + + // Create a SurfaceController to manage the state of generated surfaces. + _controller = SurfaceController(catalogs: [catalog]); + + // Create a transport adapter that will process messages to and from the + // agent, looking for A2UI messages. + _transport = A2uiTransportAdapter(onSend: _sendAndReceive); + + // Link the transport and SurfaceController together in a Conversation, + // which provides your app a unified API for interacting with the agent. + _conversation = Conversation( + controller: _controller, + transport: _transport, + ); + + // Listen to GenUI stream events to update the UI + _conversation.events.listen((event) { + setState(() { + switch (event) { + case ConversationSurfaceAdded added: + _items.add(SurfaceItem(surfaceId: added.surfaceId)); + _scrollToBottom(); + case ConversationSurfaceRemoved removed: + _items.removeWhere( + (item) => + item is SurfaceItem && item.surfaceId == removed.surfaceId, + ); + case ConversationContentReceived content: + _items.add(TextItem(text: content.text, isUser: false)); + _scrollToBottom(); + case ConversationError error: + debugPrint('GenUI Error: ${error.error}'); + default: + } + }); + }); + + // Create the system prompt for the agent, which will include this app's + // system instruction as well as the schema for the catalog. + final promptBuilder = PromptBuilder.chat( + catalog: catalog, + systemPromptFragments: [systemInstruction], + ); + + // Send the prompt into the Conversation, which will subsequently route it + // to Firebase using the transport mechanism. + _conversation.sendRequest( + ChatMessage.system(promptBuilder.systemPromptJoined()), + ); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + @override + void dispose() { + _textController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + +Future _addMessage() async { + final text = _textController.text; + + if (text.trim().isEmpty) { + return; + } + + _textController.clear(); + + setState(() { + _items.add(TextItem(text: text, isUser: true)); + }); + + _scrollToBottom(); + + // Send the user's input through GenUI instead of directly to Firebase. + await _conversation.sendRequest(ChatMessage.user(text)); + } + + Future _sendAndReceive(ChatMessage msg) async { + final buffer = StringBuffer(); + + // Reconstruct the message part fragments + for (final part in msg.parts) { + if (part.isUiInteractionPart) { + buffer.write(part.asUiInteractionPart!.interaction); + print(part.asUiInteractionPart!.interaction); + } else if (part is genui.TextPart) { + buffer.write(part.text); + } + } + + if (buffer.isEmpty) { + return; + } + + final text = buffer.toString(); + + // Send the string to Firebase AI Logic. + final response = await _chatSession.sendMessage(Content.text(text)); + + if (response.text?.isNotEmpty ?? false) { + // Feed the response back into GenUI's transportation layer + _transport.addChunk(response.text!); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Weather'), + ), + body: Column( + children: [ + Expanded( + child: ListView( + controller: _scrollController, + padding: const EdgeInsets.all(16), + children: [ + for (final item in _items) + switch (item) { + TextItem() => MessageBubble( + text: item.text, + isUser: item.isUser, + ), + SurfaceItem() => Surface( + surfaceContext: _controller.contextFor( + item.surfaceId, + ), + ), + }, + ], + ), + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _textController, + onSubmitted: (_) => _addMessage(), + decoration: const InputDecoration( + hintText: 'Enter a message', + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _addMessage, + child: const Text('Send'), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/genui_workshop/lib/message_bubble.dart b/genui_workshop/lib/message_bubble.dart new file mode 100644 index 0000000..05e4d25 --- /dev/null +++ b/genui_workshop/lib/message_bubble.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +class MessageBubble extends StatelessWidget { + final String text; + final bool isUser; + + const MessageBubble({super.key, required this.text, required this.isUser}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + final bubbleColor = isUser + ? colorScheme.primary + : colorScheme.surfaceContainerHighest; + + final textColor = isUser + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 8.0), + child: Column( + crossAxisAlignment: isUser + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: isUser + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + decoration: BoxDecoration( + color: bubbleColor, + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(20), + topRight: const Radius.circular(20), + bottomLeft: Radius.circular(isUser ? 20 : 0), + bottomRight: Radius.circular(isUser ? 0 : 20), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(20), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + gradient: isUser + ? LinearGradient( + colors: [ + colorScheme.primary, + colorScheme.primary.withAlpha(200), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + ), + child: Text( + text, + style: theme.textTheme.bodyLarge?.copyWith( + color: textColor, + height: 1.3, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 2), + ], + ), + ); + } +} \ No newline at end of file diff --git a/genui_workshop/lib/tool_calls.dart b/genui_workshop/lib/tool_calls.dart new file mode 100644 index 0000000..501cb87 --- /dev/null +++ b/genui_workshop/lib/tool_calls.dart @@ -0,0 +1,15 @@ +import 'package:firebase_ai/firebase_ai.dart'; + +final fetchWeatherGeocodeTool = FunctionDeclaration( + 'fetchWeather', + 'Retrieves the weather for the current date and returns geocoded location data', + parameters: { + 'location': Schema.string( + description: 'The location for which to retrieve the weather', + ), + 'date': Schema.string( + description: + 'The date for which to retrieve the weather', + ), + }, +); \ No newline at end of file diff --git a/genui_workshop/pubspec.yaml b/genui_workshop/pubspec.yaml new file mode 100644 index 0000000..07bb949 --- /dev/null +++ b/genui_workshop/pubspec.yaml @@ -0,0 +1,23 @@ +name: genui_workshop +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0+1 + +environment: + sdk: ^3.12.2 + +dependencies: + firebase_ai: ^3.13.0 + firebase_core: ^4.11.0 + flutter: + sdk: flutter + genui: ^0.9.2 + json_schema_builder: ^0.1.5 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true diff --git a/genui_workshop/steps.org b/genui_workshop/steps.org new file mode 100644 index 0000000..52327cd --- /dev/null +++ b/genui_workshop/steps.org @@ -0,0 +1,872 @@ +#+title: Steps + +## Step 1 + +Create a Firebase project at console.firebase.google.com. + +Enable Firebase AI logic in the project + +## Step 2: Activate Cloud Shell + +Cloud Shell space is persistent between projects so make sure you are creating things in proper project directories. + +## Step 3: Creating the project + +Cloud shell needs to link to the stored version of Flutter so we need the following command before running `flutter`. + +#+BEGIN_EXAMPLE shell +git config --global --add safe.directory /google/flutter +#+END_EXAMPLE + +We'll be starting with an empty project and adding code as we go. Warning that this might take a little while on Cloud Shell. + +#+BEGIN_EXAMPLE shell +flutter create --empty genui_workshop +#+END_EXAMPLE + +Next we can attach our local code to the previously created project with `flutterfire`. We only need the web target. + +#+BEGIN_EXAMPLE shell +dart pub global activate flutterfire_cli +export PATH="$PATH":"$HOME/.pub-cache/bin" + +cd +flutterfire configure +;; Select the project you just created +;; deselect all but web +#+END_EXAMPLE + +#+BEGIN_EXAMPLE shell +flutter pub add genui firebase_core firebase_ai json_schema_builder +#+END_EXAMPLE + +Let's run the app. + +Cloud shell doesn't allow `flutter run -d chrome` by default so we will test it by building the web target and starting a server. +#+BEGIN_EXAMPLE shell +flutter build web +cd build/web +python -m http.server 8080 + +#+END_EXAMPLE + +## Step 4: Setup GenUI scaffolding. + +Create =lib/genui_utils.dart= +#+BEGIN_EXAMPLE dart +sealed class ConversationItem {} + +class TextItem extends ConversationItem { + final String text; + final bool isUser; + TextItem({required this.text, this.isUser = false}); +} +#+END_EXAMPLE + +Create =lib/message_bubble.dart=. +#+BEGIN_EXAMPLE dart +import 'package:flutter/material.dart'; + +class MessageBubble extends StatelessWidget { + final String text; + final bool isUser; + + const MessageBubble({super.key, required this.text, required this.isUser}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + final bubbleColor = isUser + ? colorScheme.primary + : colorScheme.surfaceContainerHighest; + + final textColor = isUser + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 8.0), + child: Column( + crossAxisAlignment: isUser + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: isUser + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + decoration: BoxDecoration( + color: bubbleColor, + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(20), + topRight: const Radius.circular(20), + bottomLeft: Radius.circular(isUser ? 20 : 0), + bottomRight: Radius.circular(isUser ? 0 : 20), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(20), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + gradient: isUser + ? LinearGradient( + colors: [ + colorScheme.primary, + colorScheme.primary.withAlpha(200), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + ), + child: Text( + text, + style: theme.textTheme.bodyLarge?.copyWith( + color: textColor, + height: 1.3, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 2), + ], + ), + ); + } +} + +#+END_EXAMPLE + +Paste the following to =main.dart= +#+BEGIN_EXAMPLE dart +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:genui_workshop/message_bubble.dart'; +import 'package:genui_workshop/genui_utils.dart'; +import 'firebase_options.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Weather Today', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final List _items = []; + final _textController = TextEditingController(); + final _scrollController = ScrollController(); + late final ChatSession _chatSession; + + @override + void initState() { + super.initState(); + final model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-3.5-flash', + ); + _chatSession = model.startChat(); + _chatSession.sendMessage(Content.text(systemInstruction)); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + @override + void dispose() { + _textController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + Future _addMessage() async { + final text = _textController.text; + + if (text.trim().isEmpty) { + return; + } + _textController.clear(); + + setState(() { + _items.add(TextItem(text: text, isUser: true)); + }); + + _scrollToBottom(); + + final response = await _chatSession.sendMessage(Content.text(text)); + + if (response.text?.isNotEmpty ?? false) { + setState(() { + _items.add(TextItem(text: response.text!, isUser: false)); + }); + _scrollToBottom(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Weather'), + ), + body: Column( + children: [ + Expanded( + child: ListView( + controller: _scrollController, + padding: const EdgeInsets.all(16), + children: [ + for (final item in _items) + switch (item) { + TextItem() => MessageBubble( + text: item.text, + isUser: item.isUser, + ), + }, + ], + ), + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _textController, + onSubmitted: (_) => _addMessage(), + decoration: const InputDecoration( + hintText: 'Enter a message', + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _addMessage, + child: const Text('Send'), + ), + ], + ), + ), + ), + ], + ), + ); + } +} +#+END_EXAMPLE + + +Add the system instruction to =genui_utils.dart=. +#+BEGIN_EXAMPLE dart + const systemInstruction = ''' + ## PERSONA + You are a meteorologist. + + ## GOAL + Work with me to produce of weather forecasts. + + ## RULES + + Do not offer opinions unless I ask for them. + + ## PROCESS + ### Planning + * Ask me for a location to check the weather. + * Follow up and ask for a date if not provided. + * Synthesize a list of weather forecasts from the provided information. + * Where available, you will use tool calls to retreive the info (not implemented yet) + * Advise if you are pulling the data from a real source or making it up. + * Ask clarifying questions if you need to. + * Respond to my suggestions for changes to date or location, if I have any. +'''; +#+END_EXAMPLE + +Test app. It runs but doesn't really repsond. + +** Integrate GenUI package +Add the following to =genui_utils.dart= +#+BEGIN_EXAMPLE dart +class SurfaceItem extends ConversationItem { + final String surfaceId; + SurfaceItem({required this.surfaceId}); +} + +#+END_EXAMPLE + +In =main.dart=, add a case for the =SurfaceItem= type to the ListView =build= method: +#+BEGIN_EXAMPLE dart +Expanded( + child: ListView( + controller: _scrollController, + padding: const EdgeInsets.all(16), + children: [ + for (final item in _items) + switch (item) { + TextItem() => MessageBubble( + text: item.text, + isUser: item.isUser, + ), + // New! + SurfaceItem() => Surface( + surfaceContext: _controller.contextFor( + item.surfaceId, + ), + ), + }, + ], + ), +), +#+END_EXAMPLE + + +At the top of lib/main.dart, import the genui library: +#+begin_example dart +import 'package:genui/genui.dart' hide TextPart; +import 'package:genui/genui.dart' as genui; +#+end_example + + +Add the following lifecycle controllers and adapters to =main.dart= +#+begin_example dart +class _MyHomePageState extends State { + // ... existing members + late final ChatSession _chatSession; + + // Add GenUI controllers + late final SurfaceController _controller; + late final A2uiTransportAdapter _transport; + late final Conversation _conversation; + late final Catalog catalog; +#+end_example + +RUN. + +Update =_addMessage= and =sendAndReceive= like in code lab. + +#+begin_example dart + Future _addMessage() async { + final text = _textController.text; + + if (text.trim().isEmpty) { + return; + } + + _textController.clear(); + + setState(() { + _items.add(TextItem(text: text, isUser: true)); + }); + + _scrollToBottom(); + + // Send the user's input through GenUI instead of directly to Firebase. + await _conversation.sendRequest(ChatMessage.user(text)); + } +#+end_example + +#+begin_example dart + Future _sendAndReceive(ChatMessage msg) async { + final buffer = StringBuffer(); + + // Reconstruct the message part fragments + for (final part in msg.parts) { + if (part.isUiInteractionPart) { + buffer.write(part.asUiInteractionPart!.interaction); + } else if (part is genui.TextPart) { + buffer.write(part.text); + } + } + + if (buffer.isEmpty) { + return; + } + + final text = buffer.toString(); + + // Send the string to Firebase AI Logic. + final response = await _chatSession.sendMessage(Content.text(text)); + + if (response.text?.isNotEmpty ?? false) { + // Feed the response back into GenUI's transportation layer + _transport.addChunk(response.text!); + } + } +#+end_example + +TEST. + +### Actually wire up A2UI + +Add to the end of initState. +#+begin_example dart + // remove this line + // _chatSession.sendMessage(Content.text(systemInstruction)); + + // Initialize the GenUI Catalog. + // The genui package provides a default set of primitive widgets (like text + // and basic buttons) out of the box using this class. + catalog = BasicCatalogItems.asCatalog().copyWith( +// newItems: [weatherInput, weatherCard], + ); + + // Create a SurfaceController to manage the state of generated surfaces. + _controller = SurfaceController(catalogs: [catalog]); + + // Create a transport adapter that will process messages to and from the + // agent, looking for A2UI messages. + _transport = A2uiTransportAdapter(onSend: _sendAndReceive); + + // Link the transport and SurfaceController together in a Conversation, + // which provides your app a unified API for interacting with the agent. + _conversation = Conversation( + controller: _controller, + transport: _transport, + ); + + // Listen to GenUI stream events to update the UI + _conversation.events.listen((event) { + setState(() { + switch (event) { + case ConversationSurfaceAdded added: + _items.add(SurfaceItem(surfaceId: added.surfaceId)); + _scrollToBottom(); + case ConversationSurfaceRemoved removed: + _items.removeWhere( + (item) => + item is SurfaceItem && item.surfaceId == removed.surfaceId, + ); + case ConversationContentReceived content: + _items.add(TextItem(text: content.text, isUser: false)); + _scrollToBottom(); + case ConversationError error: + debugPrint('GenUI Error: ${error.error}'); + default: + } + }); + }); + + // Create the system prompt for the agent, which will include this app's + // system instruction as well as the schema for the catalog. + final promptBuilder = PromptBuilder.chat( + catalog: catalog, + systemPromptFragments: [systemInstruction], + ); + + // Send the prompt into the Conversation, which will subsequently route it + // to Firebase using the transport mechanism. + _conversation.sendRequest( + ChatMessage.system(promptBuilder.systemPromptJoined()), + ); +#+end_example + +We could explore doing some progress indicator UI here or not. + +** Make weather input widget +The components it picks is random and can't be submitted sometimes + +Create a =catalog= directory. One file per item with the schema included. + +Add the following to =catalog/weather_input.dart= +#+begin_example dart +import 'package:flutter/material.dart'; +import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +final simpleWeatherSchema = S.object( + properties: { + 'location': S.string(description: 'The location to check the weather.'), + 'date': S.string(description: 'The date to check the weather.'), + }, +); + +// Example raw action +//{version: v0.9, action: {name: submit_weather_request, sourceComponentId: submitButton, +//timestamp: 2026-05-26T22:10:19.087, context: {}, surfaceId: weather_input_surface}} +final weatherInput = CatalogItem( + name: 'WeatherInput', + dataSchema: simpleWeatherSchema, + widgetBuilder: (itemContext) { + final json = itemContext.data as Map; + final data = SimpleWeatherData.fromJson(json); + return WeatherInput( + data: data, + onFetchRequest: (loc, date) async { + final JsonMap resolvedContext = await resolveContext( + itemContext.dataContext, + {'location': loc, 'date': date.toString()}, + ); + itemContext.dispatchEvent( + UserActionEvent( + name: 'submit_weather_request', + sourceComponentId: 'submitButton', + timestamp: DateTime.now(), + context: resolvedContext + ), + ); + }, + ); + }, +); + +class SimpleWeatherData { + String location; + DateTime date; + + SimpleWeatherData({required this.location, required this.date}); + + factory SimpleWeatherData.defaultValues() { + return SimpleWeatherData(location: 'Baltimore', date: DateTime.now()); + } + + factory SimpleWeatherData.fromJson(Map json) { + if (json.isNotEmpty) { + return SimpleWeatherData( + location: json['location'] as String, + date: DateTime.parse(json['date'] as String), + ); + } else { + + return SimpleWeatherData(location: 'Baltimore', date: DateTime.now()); + } + } +} + +class WeatherInput extends StatefulWidget { + final SimpleWeatherData data; + final void Function(String, DateTime) onFetchRequest; + + const WeatherInput({ + super.key, + required this.data, + required this.onFetchRequest, + }); + + @override + State createState() => _WeatherInputState(); +} + +class _WeatherInputState extends State { + late TextEditingController _controller; + DateTime? selectedDate = DateTime.now(); + + Future _selectDate() async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now().copyWith(month: 1, day: 1), + lastDate: DateTime.now().copyWith(month: 12, day: 31), + ); + + setState(() { + selectedDate = pickedDate; + }); + } + + @override + void initState() { + // TODO: implement initState + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + + @override + Widget build(BuildContext context) { + return Container( + width: 320, + padding: const EdgeInsets.all(16), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Location", style: TextStyle(fontWeight: FontWeight.bold) ), + const SizedBox(height: 8), + TextField(decoration: InputDecoration(border: OutlineInputBorder()), controller: _controller,), + const SizedBox(height: 20), + /* const Text("Date", style: TextStyle( fontWeight: FontWeight.bold)), + const SizedBox(height: 8), +Row( + children: [ + Text(selectedDate != null ? '$selectedDate' : 'No date selected'), + const SizedBox(width: 16.0), + IconButton(onPressed: _selectDate, icon: Icon(Icons.calendar_today_outlined),) + + ], + ),*/ + // const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 50, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), + ), + onPressed: () { + + widget.onFetchRequest(_controller.text, selectedDate!); + }, + child: const Text("Get Forecast", style: TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ), + ), + ); + } +} +#+end_example +- Add weatherInput as a new item in the basic catalog. +- Change the system prompt to point it to the catalog item + +Add this to end of the system instruction prompt. +#+begin_example dart + + ## USER INTERFACE + * To request the location to retreive weather, create an instance of the WeatherInput + catalog item. +#+end_example + +Test it. + +#+begin_example dart +import 'package:flutter/material.dart'; +import 'package:genui_workshop/genui_utils.dart'; +import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'dart:io'; + +import 'dart:convert'; +import 'fake_forecast.dart'; + +final forecastSchema = S.object( + properties: { + 'area_name': S.string(), + //"flags": S.object(), + 'current_condition': S.object( + properties: { + "temp_C": S.number(), + "temp_F": S.number(), + "humidity": S.number(), + "observation_time": S.string(), + }, + ), + 'weatherDesc': S.string(), + 'weatherIconUrl': S.string(), + }, +); + +final weatherCard = CatalogItem( + name: 'WeatherCard', + dataSchema: forecastSchema, + widgetBuilder: (itemContext) { + return WeatherCard(data: itemContext.data as Map); + }, +); + +class WeatherCard extends StatelessWidget { + final Map data; + const WeatherCard({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + final currentCondition = data["current_condition"] as Map; + return Container( + width: 500, + padding: const EdgeInsets.all(20), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: .end, + children: [ + Text( + "Observed at ${currentCondition["observation_time"]}", + style: TextStyle(fontSize: 12), + ), + ], + ), + const SizedBox(height: 8), + // Header: Icon and City Name + Row( + children: [ + Image.network( + data["weatherIconUrl"].toString(), + errorBuilder: (context, err, trace) { + return Icon(Icons.wb_sunny, size: 32,); + }, + ), + const SizedBox(width: 16), + Text( + data["area_name"].toString(), + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 24), + + // Temperature Display + Row( + children: [ + Text( + currentCondition["temp_C"]!.toString(), + style: TextStyle(fontSize: 56, fontWeight: FontWeight.bold), + ), + SizedBox(width: 8), + Text( + "°C", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.w500), + ), + ], + ), + const SizedBox(height: 16), + + // Low Temp row + Row( + children: [ + const SizedBox(width: 16), + Text( + currentCondition['temp_F'].toString(), + style: TextStyle(fontSize: 28, fontWeight: FontWeight.w500), + ), + SizedBox(width: 8), + Text( + "°F", + style: TextStyle(fontSize: 20, color: Colors.grey), + ), + ], + ), + const SizedBox(height: 16), + + // Weather Condition + Text("${data['weatherDesc']}", style: TextStyle(fontSize: 22)), + ], + ), + ), + ), + ); + } +} +#+end_example + +Create a file called =catalog/fake_forecast.dart= +#+begin_example dart +var fakeForecast = { + 'area_name': 'Baltimore (Mount Clare)', + 'current_condition': { + 'temp_C': 22, + 'temp_F': 72, + 'humidity': 61, + 'observation_time': '02:25 PM', + }, + 'weatherDesc': ' Sunny', + 'weatherIconUrl': + 'https://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0001_sunny.png', +}; +#+end_example + +## Functions + +Good area to split to another tutorial. + +Create a file named =lib/tool_calls.dart=. + +#+begin_example dart +import 'package:firebase_ai/firebase_ai.dart'; + +final fetchWeatherGeocodeTool = FunctionDeclaration( + 'fetchWeather', + 'Retrieves the weather for the current date and returns geocoded location data', + parameters: { + 'location': Schema.string( + description: 'The location for which to retrieve the weather', + ), + 'date': Schema.string( + description: + 'The date for which to retrieve the weather', + ), + }, +); +#+end_example + +Add the import to the top of =main.dart=. +#+begin_example dart +import 'package:genui_workshop/tool_calls.dart'; +#+end_example + +Update the model in =initState= from main.dart to the following: +#+begin_example dart +final model = FirebaseAI.googleAI().generativeModel( + //model: 'gemini-3.5-flash', + model: 'gemini-3.1-flash-lite', + // tools that will call cloud functions + tools: [ + Tool.functionDeclarations([fetchWeatherGeocodeTool]), + ], + ); +#+end_example + +.... +TBD diff --git a/genui_workshop/web/favicon.png b/genui_workshop/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/genui_workshop/web/favicon.png differ diff --git a/genui_workshop/web/icons/Icon-192.png b/genui_workshop/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/genui_workshop/web/icons/Icon-192.png differ diff --git a/genui_workshop/web/icons/Icon-512.png b/genui_workshop/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/genui_workshop/web/icons/Icon-512.png differ diff --git a/genui_workshop/web/icons/Icon-maskable-192.png b/genui_workshop/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/genui_workshop/web/icons/Icon-maskable-192.png differ diff --git a/genui_workshop/web/icons/Icon-maskable-512.png b/genui_workshop/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/genui_workshop/web/icons/Icon-maskable-512.png differ diff --git a/genui_workshop/web/index.html b/genui_workshop/web/index.html new file mode 100644 index 0000000..db370f9 --- /dev/null +++ b/genui_workshop/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + genui_workshop + + + + + + + diff --git a/genui_workshop/web/manifest.json b/genui_workshop/web/manifest.json new file mode 100644 index 0000000..89706d1 --- /dev/null +++ b/genui_workshop/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "genui_workshop", + "short_name": "genui_workshop", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/native_interop_demos/android_launch_activity/.gitignore b/native_interop_demos/android_launch_activity/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/native_interop_demos/android_launch_activity/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/native_interop_demos/android_launch_activity/.metadata b/native_interop_demos/android_launch_activity/.metadata new file mode 100644 index 0000000..abb4ead --- /dev/null +++ b/native_interop_demos/android_launch_activity/.metadata @@ -0,0 +1,30 @@ +# 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: "67323de285b00232883f53b84095eb72be97d35c" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 67323de285b00232883f53b84095eb72be97d35c + base_revision: 67323de285b00232883f53b84095eb72be97d35c + - platform: android + create_revision: 67323de285b00232883f53b84095eb72be97d35c + base_revision: 67323de285b00232883f53b84095eb72be97d35c + + # 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' diff --git a/native_interop_demos/android_launch_activity/README.md b/native_interop_demos/android_launch_activity/README.md new file mode 100644 index 0000000..86966f1 --- /dev/null +++ b/native_interop_demos/android_launch_activity/README.md @@ -0,0 +1,16 @@ +# android_launch_activity + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/native_interop_demos/android_launch_activity/analysis_options.yaml b/native_interop_demos/android_launch_activity/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/native_interop_demos/android_launch_activity/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/native_interop_demos/android_launch_activity/android/.gitignore b/native_interop_demos/android_launch_activity/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/native_interop_demos/android_launch_activity/android/app/build.gradle.kts b/native_interop_demos/android_launch_activity/android/app/build.gradle.kts new file mode 100644 index 0000000..e9393d9 --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/app/build.gradle.kts @@ -0,0 +1,68 @@ +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") + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + buildFeatures { + compose = true + } + namespace = "com.example.android_launch_activity" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.android_launch_activity" + // 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 + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} + +dependencies { + val composeBom = platform("androidx.compose:compose-bom:2026.01.01") + implementation(composeBom) + testImplementation(composeBom) + androidTestImplementation(composeBom) + + implementation("androidx.activity:activity-compose") + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.foundation:foundation-layout") // often the issue source + + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material:material") + implementation("androidx.compose.material3:material3") + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/native_interop_demos/android_launch_activity/android/app/src/debug/AndroidManifest.xml b/native_interop_demos/android_launch_activity/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/AndroidManifest.xml b/native_interop_demos/android_launch_activity/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..db12e08 --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/kotlin/com/example/android_launch_activity/MainActivity.kt b/native_interop_demos/android_launch_activity/android/app/src/main/kotlin/com/example/android_launch_activity/MainActivity.kt new file mode 100644 index 0000000..2a36a53 --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/app/src/main/kotlin/com/example/android_launch_activity/MainActivity.kt @@ -0,0 +1,38 @@ +package com.example.android_launch_activity + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.core.app.ActivityCompat +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() + +class SecondActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MaterialTheme { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + Column { + Text(text = "Second Activity") + // Note: This must match the shape of the data passed from your Dart code. + Text("" + getIntent()?.getExtras()?.getString("message")) + Button(onClick = { finish() }) { + Text("Exit") + } + } + } + } + } + } +} diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/res/drawable-v21/launch_background.xml b/native_interop_demos/android_launch_activity/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/res/drawable/launch_background.xml b/native_interop_demos/android_launch_activity/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/native_interop_demos/android_launch_activity/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/res/values-night/styles.xml b/native_interop_demos/android_launch_activity/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/native_interop_demos/android_launch_activity/android/app/src/main/res/values/styles.xml b/native_interop_demos/android_launch_activity/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/native_interop_demos/android_launch_activity/android/app/src/profile/AndroidManifest.xml b/native_interop_demos/android_launch_activity/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/native_interop_demos/android_launch_activity/android/build.gradle.kts b/native_interop_demos/android_launch_activity/android/build.gradle.kts new file mode 100644 index 0000000..1534bc7 --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/build.gradle.kts @@ -0,0 +1,35 @@ +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("clean") { + delete(rootProject.layout.buildDirectory) +} + +buildscript { + dependencies { + // Replace with the latest version. + classpath("com.android.tools.build:gradle:8.1.1") + } + repositories { + google() + mavenCentral() + } +} diff --git a/native_interop_demos/android_launch_activity/android/gradle.properties b/native_interop_demos/android_launch_activity/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/native_interop_demos/android_launch_activity/android/gradle/wrapper/gradle-wrapper.properties b/native_interop_demos/android_launch_activity/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/gradle/wrapper/gradle-wrapper.properties @@ -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-all.zip diff --git a/native_interop_demos/android_launch_activity/android/settings.gradle.kts b/native_interop_demos/android_launch_activity/android/settings.gradle.kts new file mode 100644 index 0000000..4a99cd2 --- /dev/null +++ b/native_interop_demos/android_launch_activity/android/settings.gradle.kts @@ -0,0 +1,27 @@ +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.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false +} + +include(":app") diff --git a/native_interop_demos/android_launch_activity/lib/gen/android.g.dart b/native_interop_demos/android_launch_activity/lib/gen/android.g.dart new file mode 100644 index 0000000..acea952 --- /dev/null +++ b/native_interop_demos/android_launch_activity/lib/gen/android.g.dart @@ -0,0 +1,10512 @@ +// AUTO GENERATED BY JNIGEN 0.15.0. DO NOT EDIT! + + +// ignore_for_file: annotate_overrides +// ignore_for_file: argument_type_not_assignable +// ignore_for_file: camel_case_extensions +// ignore_for_file: camel_case_types +// ignore_for_file: constant_identifier_names +// ignore_for_file: comment_references +// ignore_for_file: doc_directive_unknown +// ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter +// ignore_for_file: invalid_internal_annotation +// ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: library_prefixes +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: no_leading_underscores_for_library_prefixes +// ignore_for_file: no_leading_underscores_for_local_identifiers +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: only_throw_errors +// ignore_for_file: overridden_fields +// ignore_for_file: prefer_double_quotes +// ignore_for_file: unintended_html_in_doc_comment +// ignore_for_file: unnecessary_cast +// ignore_for_file: unnecessary_non_null_assertion +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: unused_element +// ignore_for_file: unused_field +// ignore_for_file: unused_import +// ignore_for_file: unused_local_variable +// ignore_for_file: unused_shown_name +// ignore_for_file: use_super_parameters + + +import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; + +import 'package:jni/_internal.dart' as jni$_; +import 'package:jni/jni.dart' as jni$_; + + +/// from: `android.os.Bundle` +class Bundle extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + + + @jni$_.internal + Bundle.fromReference( + + jni$_.JReference reference, + ) : + $type = type, + super.fromReference( + + reference + ); + + static final _class = jni$_.JClass.forName(r'android/os/Bundle'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Bundle$NullableType$(); + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Bundle$Type$(); + static final _id_CREATOR = + _class.staticFieldId( + r'CREATOR', + r'Landroid/os/Parcelable$Creator;', + ); + /// from: `static public final android.os.Parcelable$Creator CREATOR` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JObject? get CREATOR => _id_CREATOR.get(_class, const jni$_.$JObject$NullableType$()); + + static final _id_EMPTY = + _class.staticFieldId( + r'EMPTY', + r'Landroid/os/Bundle;', + ); + /// from: `static public final android.os.Bundle EMPTY` + /// The returned object must be released after use, by calling the [release] method. +static Bundle? get EMPTY => _id_EMPTY.get(_class, const $Bundle$NullableType$()); + + static final _id_new$ = _class.constructorId( + r'()V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory Bundle() { + + + return Bundle.fromReference( + + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference + ); + } + + static final _id_new$1 = _class.constructorId( + r'(Landroid/os/Bundle;)V', + ); + + static final _new$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void (android.os.Bundle bundle)` + /// The returned object must be released after use, by calling the [release] method. + factory Bundle.new$1(Bundle? bundle, ) { + + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return Bundle.fromReference( + + _new$1(_class.reference.pointer, _id_new$1 as jni$_.JMethodIDPtr, _$bundle.pointer).reference + ); + } + + static final _id_new$2 = _class.constructorId( + r'(Landroid/os/PersistableBundle;)V', + ); + + static final _new$2 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void (android.os.PersistableBundle persistableBundle)` + /// The returned object must be released after use, by calling the [release] method. + factory Bundle.new$2(jni$_.JObject? persistableBundle, ) { + + final _$persistableBundle = persistableBundle?.reference ?? jni$_.jNullReference; + return Bundle.fromReference( + + _new$2(_class.reference.pointer, _id_new$2 as jni$_.JMethodIDPtr, _$persistableBundle.pointer).reference + ); + } + + static final _id_new$3 = _class.constructorId( + r'(I)V', + ); + + static final _new$3 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public void (int i)` + /// The returned object must be released after use, by calling the [release] method. + factory Bundle.new$3(int i, ) { + + + return Bundle.fromReference( + + _new$3(_class.reference.pointer, _id_new$3 as jni$_.JMethodIDPtr, i).reference + ); + } + + static final _id_new$4 = _class.constructorId( + r'(Ljava/lang/ClassLoader;)V', + ); + + static final _new$4 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void (java.lang.ClassLoader classLoader)` + /// The returned object must be released after use, by calling the [release] method. + factory Bundle.new$4(jni$_.JObject? classLoader, ) { + + final _$classLoader = classLoader?.reference ?? jni$_.jNullReference; + return Bundle.fromReference( + + _new$4(_class.reference.pointer, _id_new$4 as jni$_.JMethodIDPtr, _$classLoader.pointer).reference + ); + } + + static final _id_clear = _class.instanceMethodId( + r'clear', + r'()V', + ); + + static final _clear = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void clear()` + void clear(){ + + + _clear(reference.pointer, _id_clear as jni$_.JMethodIDPtr).check(); + } + + static final _id_clone = _class.instanceMethodId( + r'clone', + r'()Ljava/lang/Object;', + ); + + static final _clone = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.Object clone()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? clone(){ + + + return _clone(reference.pointer, _id_clone as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_deepCopy = _class.instanceMethodId( + r'deepCopy', + r'()Landroid/os/Bundle;', + ); + + static final _deepCopy = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.os.Bundle deepCopy()` + /// The returned object must be released after use, by calling the [release] method. + Bundle? deepCopy(){ + + + return _deepCopy(reference.pointer, _id_deepCopy as jni$_.JMethodIDPtr).object(const $Bundle$NullableType$()); + } + + static final _id_describeContents = _class.instanceMethodId( + r'describeContents', + r'()I', + ); + + static final _describeContents = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int describeContents()` + int describeContents(){ + + + return _describeContents(reference.pointer, _id_describeContents as jni$_.JMethodIDPtr).integer; + } + + static final _id_getBinder = _class.instanceMethodId( + r'getBinder', + r'(Ljava/lang/String;)Landroid/os/IBinder;', + ); + + static final _getBinder = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.os.IBinder getBinder(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getBinder(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getBinder(reference.pointer, _id_getBinder as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getBundle = _class.instanceMethodId( + r'getBundle', + r'(Ljava/lang/String;)Landroid/os/Bundle;', + ); + + static final _getBundle = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.os.Bundle getBundle(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Bundle? getBundle(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getBundle(reference.pointer, _id_getBundle as jni$_.JMethodIDPtr, _$string.pointer).object(const $Bundle$NullableType$()); + } + + static final _id_getByte = _class.instanceMethodId( + r'getByte', + r'(Ljava/lang/String;)B', + ); + + static final _getByte = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallByteMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public byte getByte(java.lang.String string)` + int getByte(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getByte(reference.pointer, _id_getByte as jni$_.JMethodIDPtr, _$string.pointer).byte; + } + + static final _id_getByte$1 = _class.instanceMethodId( + r'getByte', + r'(Ljava/lang/String;B)Ljava/lang/Byte;', + ); + + static final _getByte$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public java.lang.Byte getByte(java.lang.String string, byte b)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JByte? getByte$1(jni$_.JString? string, int b, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getByte$1(reference.pointer, _id_getByte$1 as jni$_.JMethodIDPtr, _$string.pointer, b).object(const jni$_.$JByte$NullableType$()); + } + + static final _id_getByteArray = _class.instanceMethodId( + r'getByteArray', + r'(Ljava/lang/String;)[B', + ); + + static final _getByteArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public byte[] getByteArray(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JByteArray? getByteArray(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getByteArray(reference.pointer, _id_getByteArray as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JByteArray$NullableType$()); + } + + static final _id_getChar = _class.instanceMethodId( + r'getChar', + r'(Ljava/lang/String;)C', + ); + + static final _getChar = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallCharMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public char getChar(java.lang.String string)` + int getChar(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getChar(reference.pointer, _id_getChar as jni$_.JMethodIDPtr, _$string.pointer).char; + } + + static final _id_getChar$1 = _class.instanceMethodId( + r'getChar', + r'(Ljava/lang/String;C)C', + ); + + static final _getChar$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallCharMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public char getChar(java.lang.String string, char c)` + int getChar$1(jni$_.JString? string, int c, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getChar$1(reference.pointer, _id_getChar$1 as jni$_.JMethodIDPtr, _$string.pointer, c).char; + } + + static final _id_getCharArray = _class.instanceMethodId( + r'getCharArray', + r'(Ljava/lang/String;)[C', + ); + + static final _getCharArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public char[] getCharArray(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JCharArray? getCharArray(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharArray(reference.pointer, _id_getCharArray as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JCharArray$NullableType$()); + } + + static final _id_getCharSequence = _class.instanceMethodId( + r'getCharSequence', + r'(Ljava/lang/String;)Ljava/lang/CharSequence;', + ); + + static final _getCharSequence = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.lang.CharSequence getCharSequence(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCharSequence(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharSequence(reference.pointer, _id_getCharSequence as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getCharSequence$1 = _class.instanceMethodId( + r'getCharSequence', + r'(Ljava/lang/String;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;', + ); + + static final _getCharSequence$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public java.lang.CharSequence getCharSequence(java.lang.String string, java.lang.CharSequence charSequence)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCharSequence$1(jni$_.JString? string, jni$_.JObject? charSequence, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + return _getCharSequence$1(reference.pointer, _id_getCharSequence$1 as jni$_.JMethodIDPtr, _$string.pointer, _$charSequence.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getCharSequenceArray = _class.instanceMethodId( + r'getCharSequenceArray', + r'(Ljava/lang/String;)[Ljava/lang/CharSequence;', + ); + + static final _getCharSequenceArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.lang.CharSequence[] getCharSequenceArray(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getCharSequenceArray(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharSequenceArray(reference.pointer, _id_getCharSequenceArray as jni$_.JMethodIDPtr, _$string.pointer).object?>(const jni$_.$JArray$NullableType$(jni$_.$JObject$NullableType$())); + } + + static final _id_getCharSequenceArrayList = _class.instanceMethodId( + r'getCharSequenceArrayList', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getCharSequenceArrayList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getCharSequenceArrayList(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCharSequenceArrayList(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharSequenceArrayList(reference.pointer, _id_getCharSequenceArrayList as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getClassLoader = _class.instanceMethodId( + r'getClassLoader', + r'()Ljava/lang/ClassLoader;', + ); + + static final _getClassLoader = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.ClassLoader getClassLoader()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getClassLoader(){ + + + return _getClassLoader(reference.pointer, _id_getClassLoader as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getFloat = _class.instanceMethodId( + r'getFloat', + r'(Ljava/lang/String;)F', + ); + + static final _getFloat = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallFloatMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public float getFloat(java.lang.String string)` + double getFloat(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getFloat(reference.pointer, _id_getFloat as jni$_.JMethodIDPtr, _$string.pointer).float; + } + + static final _id_getFloat$1 = _class.instanceMethodId( + r'getFloat', + r'(Ljava/lang/String;F)F', + ); + + static final _getFloat$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>)>>('globalEnv_CallFloatMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, double)>(); + /// from: `public float getFloat(java.lang.String string, float f)` + double getFloat$1(jni$_.JString? string, double f, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getFloat$1(reference.pointer, _id_getFloat$1 as jni$_.JMethodIDPtr, _$string.pointer, f).float; + } + + static final _id_getFloatArray = _class.instanceMethodId( + r'getFloatArray', + r'(Ljava/lang/String;)[F', + ); + + static final _getFloatArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public float[] getFloatArray(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JFloatArray? getFloatArray(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getFloatArray(reference.pointer, _id_getFloatArray as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JFloatArray$NullableType$()); + } + + static final _id_getIntegerArrayList = _class.instanceMethodId( + r'getIntegerArrayList', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getIntegerArrayList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getIntegerArrayList(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getIntegerArrayList(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntegerArrayList(reference.pointer, _id_getIntegerArrayList as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getParcelable = _class.instanceMethodId( + r'getParcelable', + r'(Ljava/lang/String;)Landroid/os/Parcelable;', + ); + + static final _getParcelable = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public T getParcelable(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + $T? getParcelable<$T extends jni$_.JObject?>(jni$_.JString? string, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getParcelable(reference.pointer, _id_getParcelable as jni$_.JMethodIDPtr, _$string.pointer).object<$T?>(T.nullableType); + } + + static final _id_getParcelable$1 = _class.instanceMethodId( + r'getParcelable', + r'(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;', + ); + + static final _getParcelable$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public T getParcelable(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + $T? getParcelable$1<$T extends jni$_.JObject?>(jni$_.JString? string, jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getParcelable$1(reference.pointer, _id_getParcelable$1 as jni$_.JMethodIDPtr, _$string.pointer, _$class$.pointer).object<$T?>(T.nullableType); + } + + static final _id_getParcelableArray = _class.instanceMethodId( + r'getParcelableArray', + r'(Ljava/lang/String;)[Landroid/os/Parcelable;', + ); + + static final _getParcelableArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.os.Parcelable[] getParcelableArray(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getParcelableArray(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getParcelableArray(reference.pointer, _id_getParcelableArray as jni$_.JMethodIDPtr, _$string.pointer).object?>(const jni$_.$JArray$NullableType$(jni$_.$JObject$NullableType$())); + } + + static final _id_getParcelableArray$1 = _class.instanceMethodId( + r'getParcelableArray', + r'(Ljava/lang/String;Ljava/lang/Class;)[Ljava/lang/Object;', + ); + + static final _getParcelableArray$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public T[] getParcelableArray(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray<$T?>? getParcelableArray$1<$T extends jni$_.JObject?>(jni$_.JString? string, jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getParcelableArray$1(reference.pointer, _id_getParcelableArray$1 as jni$_.JMethodIDPtr, _$string.pointer, _$class$.pointer).object?>(jni$_.$JArray$NullableType$<$T?>(T.nullableType)); + } + + static final _id_getParcelableArrayList = _class.instanceMethodId( + r'getParcelableArrayList', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getParcelableArrayList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getParcelableArrayList(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getParcelableArrayList<$T extends jni$_.JObject?>(jni$_.JString? string, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getParcelableArrayList(reference.pointer, _id_getParcelableArrayList as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getParcelableArrayList$1 = _class.instanceMethodId( + r'getParcelableArrayList', + r'(Ljava/lang/String;Ljava/lang/Class;)Ljava/util/ArrayList;', + ); + + static final _getParcelableArrayList$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getParcelableArrayList(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getParcelableArrayList$1<$T extends jni$_.JObject?>(jni$_.JString? string, jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getParcelableArrayList$1(reference.pointer, _id_getParcelableArrayList$1 as jni$_.JMethodIDPtr, _$string.pointer, _$class$.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSerializable = _class.instanceMethodId( + r'getSerializable', + r'(Ljava/lang/String;)Ljava/io/Serializable;', + ); + + static final _getSerializable = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.io.Serializable getSerializable(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSerializable(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getSerializable(reference.pointer, _id_getSerializable as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSerializable$1 = _class.instanceMethodId( + r'getSerializable', + r'(Ljava/lang/String;Ljava/lang/Class;)Ljava/io/Serializable;', + ); + + static final _getSerializable$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public T getSerializable(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + $T? getSerializable$1<$T extends jni$_.JObject?>(jni$_.JString? string, jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getSerializable$1(reference.pointer, _id_getSerializable$1 as jni$_.JMethodIDPtr, _$string.pointer, _$class$.pointer).object<$T?>(T.nullableType); + } + + static final _id_getShort = _class.instanceMethodId( + r'getShort', + r'(Ljava/lang/String;)S', + ); + + static final _getShort = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallShortMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public short getShort(java.lang.String string)` + int getShort(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getShort(reference.pointer, _id_getShort as jni$_.JMethodIDPtr, _$string.pointer).short; + } + + static final _id_getShort$1 = _class.instanceMethodId( + r'getShort', + r'(Ljava/lang/String;S)S', + ); + + static final _getShort$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallShortMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public short getShort(java.lang.String string, short s)` + int getShort$1(jni$_.JString? string, int s, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getShort$1(reference.pointer, _id_getShort$1 as jni$_.JMethodIDPtr, _$string.pointer, s).short; + } + + static final _id_getShortArray = _class.instanceMethodId( + r'getShortArray', + r'(Ljava/lang/String;)[S', + ); + + static final _getShortArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public short[] getShortArray(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JShortArray? getShortArray(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getShortArray(reference.pointer, _id_getShortArray as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JShortArray$NullableType$()); + } + + static final _id_getSize = _class.instanceMethodId( + r'getSize', + r'(Ljava/lang/String;)Landroid/util/Size;', + ); + + static final _getSize = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.util.Size getSize(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSize(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getSize(reference.pointer, _id_getSize as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSizeF = _class.instanceMethodId( + r'getSizeF', + r'(Ljava/lang/String;)Landroid/util/SizeF;', + ); + + static final _getSizeF = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.util.SizeF getSizeF(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSizeF(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getSizeF(reference.pointer, _id_getSizeF as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSparseParcelableArray = _class.instanceMethodId( + r'getSparseParcelableArray', + r'(Ljava/lang/String;)Landroid/util/SparseArray;', + ); + + static final _getSparseParcelableArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.util.SparseArray getSparseParcelableArray(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSparseParcelableArray<$T extends jni$_.JObject?>(jni$_.JString? string, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getSparseParcelableArray(reference.pointer, _id_getSparseParcelableArray as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSparseParcelableArray$1 = _class.instanceMethodId( + r'getSparseParcelableArray', + r'(Ljava/lang/String;Ljava/lang/Class;)Landroid/util/SparseArray;', + ); + + static final _getSparseParcelableArray$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.util.SparseArray getSparseParcelableArray(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSparseParcelableArray$1<$T extends jni$_.JObject?>(jni$_.JString? string, jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getSparseParcelableArray$1(reference.pointer, _id_getSparseParcelableArray$1 as jni$_.JMethodIDPtr, _$string.pointer, _$class$.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getStringArrayList = _class.instanceMethodId( + r'getStringArrayList', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getStringArrayList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getStringArrayList(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getStringArrayList(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getStringArrayList(reference.pointer, _id_getStringArrayList as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_hasFileDescriptors = _class.instanceMethodId( + r'hasFileDescriptors', + r'()Z', + ); + + static final _hasFileDescriptors = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public boolean hasFileDescriptors()` + bool hasFileDescriptors(){ + + + return _hasFileDescriptors(reference.pointer, _id_hasFileDescriptors as jni$_.JMethodIDPtr).boolean; + } + + static final _id_putAll = _class.instanceMethodId( + r'putAll', + r'(Landroid/os/Bundle;)V', + ); + + static final _putAll = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void putAll(android.os.Bundle bundle)` + void putAll(Bundle? bundle, ){ + + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _putAll(reference.pointer, _id_putAll as jni$_.JMethodIDPtr, _$bundle.pointer).check(); + } + + static final _id_putBinder = _class.instanceMethodId( + r'putBinder', + r'(Ljava/lang/String;Landroid/os/IBinder;)V', + ); + + static final _putBinder = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putBinder(java.lang.String string, android.os.IBinder iBinder)` + void putBinder(jni$_.JString? string, jni$_.JObject? iBinder, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$iBinder = iBinder?.reference ?? jni$_.jNullReference; + _putBinder(reference.pointer, _id_putBinder as jni$_.JMethodIDPtr, _$string.pointer, _$iBinder.pointer).check(); + } + + static final _id_putBundle = _class.instanceMethodId( + r'putBundle', + r'(Ljava/lang/String;Landroid/os/Bundle;)V', + ); + + static final _putBundle = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putBundle(java.lang.String string, android.os.Bundle bundle)` + void putBundle(jni$_.JString? string, Bundle? bundle, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _putBundle(reference.pointer, _id_putBundle as jni$_.JMethodIDPtr, _$string.pointer, _$bundle.pointer).check(); + } + + static final _id_putByte = _class.instanceMethodId( + r'putByte', + r'(Ljava/lang/String;B)V', + ); + + static final _putByte = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public void putByte(java.lang.String string, byte b)` + void putByte(jni$_.JString? string, int b, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + _putByte(reference.pointer, _id_putByte as jni$_.JMethodIDPtr, _$string.pointer, b).check(); + } + + static final _id_putByteArray = _class.instanceMethodId( + r'putByteArray', + r'(Ljava/lang/String;[B)V', + ); + + static final _putByteArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putByteArray(java.lang.String string, byte[] bs)` + void putByteArray(jni$_.JString? string, jni$_.JByteArray? bs, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bs = bs?.reference ?? jni$_.jNullReference; + _putByteArray(reference.pointer, _id_putByteArray as jni$_.JMethodIDPtr, _$string.pointer, _$bs.pointer).check(); + } + + static final _id_putChar = _class.instanceMethodId( + r'putChar', + r'(Ljava/lang/String;C)V', + ); + + static final _putChar = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public void putChar(java.lang.String string, char c)` + void putChar(jni$_.JString? string, int c, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + _putChar(reference.pointer, _id_putChar as jni$_.JMethodIDPtr, _$string.pointer, c).check(); + } + + static final _id_putCharArray = _class.instanceMethodId( + r'putCharArray', + r'(Ljava/lang/String;[C)V', + ); + + static final _putCharArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putCharArray(java.lang.String string, char[] cs)` + void putCharArray(jni$_.JString? string, jni$_.JCharArray? cs, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$cs = cs?.reference ?? jni$_.jNullReference; + _putCharArray(reference.pointer, _id_putCharArray as jni$_.JMethodIDPtr, _$string.pointer, _$cs.pointer).check(); + } + + static final _id_putCharSequence = _class.instanceMethodId( + r'putCharSequence', + r'(Ljava/lang/String;Ljava/lang/CharSequence;)V', + ); + + static final _putCharSequence = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putCharSequence(java.lang.String string, java.lang.CharSequence charSequence)` + void putCharSequence(jni$_.JString? string, jni$_.JObject? charSequence, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + _putCharSequence(reference.pointer, _id_putCharSequence as jni$_.JMethodIDPtr, _$string.pointer, _$charSequence.pointer).check(); + } + + static final _id_putCharSequenceArray = _class.instanceMethodId( + r'putCharSequenceArray', + r'(Ljava/lang/String;[Ljava/lang/CharSequence;)V', + ); + + static final _putCharSequenceArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putCharSequenceArray(java.lang.String string, java.lang.CharSequence[] charSequences)` + void putCharSequenceArray(jni$_.JString? string, jni$_.JArray? charSequences, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$charSequences = charSequences?.reference ?? jni$_.jNullReference; + _putCharSequenceArray(reference.pointer, _id_putCharSequenceArray as jni$_.JMethodIDPtr, _$string.pointer, _$charSequences.pointer).check(); + } + + static final _id_putCharSequenceArrayList = _class.instanceMethodId( + r'putCharSequenceArrayList', + r'(Ljava/lang/String;Ljava/util/ArrayList;)V', + ); + + static final _putCharSequenceArrayList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putCharSequenceArrayList(java.lang.String string, java.util.ArrayList arrayList)` + void putCharSequenceArrayList(jni$_.JString? string, jni$_.JObject? arrayList, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + _putCharSequenceArrayList(reference.pointer, _id_putCharSequenceArrayList as jni$_.JMethodIDPtr, _$string.pointer, _$arrayList.pointer).check(); + } + + static final _id_putFloat = _class.instanceMethodId( + r'putFloat', + r'(Ljava/lang/String;F)V', + ); + + static final _putFloat = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, double)>(); + /// from: `public void putFloat(java.lang.String string, float f)` + void putFloat(jni$_.JString? string, double f, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + _putFloat(reference.pointer, _id_putFloat as jni$_.JMethodIDPtr, _$string.pointer, f).check(); + } + + static final _id_putFloatArray = _class.instanceMethodId( + r'putFloatArray', + r'(Ljava/lang/String;[F)V', + ); + + static final _putFloatArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putFloatArray(java.lang.String string, float[] fs)` + void putFloatArray(jni$_.JString? string, jni$_.JFloatArray? fs, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$fs = fs?.reference ?? jni$_.jNullReference; + _putFloatArray(reference.pointer, _id_putFloatArray as jni$_.JMethodIDPtr, _$string.pointer, _$fs.pointer).check(); + } + + static final _id_putIntegerArrayList = _class.instanceMethodId( + r'putIntegerArrayList', + r'(Ljava/lang/String;Ljava/util/ArrayList;)V', + ); + + static final _putIntegerArrayList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putIntegerArrayList(java.lang.String string, java.util.ArrayList arrayList)` + void putIntegerArrayList(jni$_.JString? string, jni$_.JObject? arrayList, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + _putIntegerArrayList(reference.pointer, _id_putIntegerArrayList as jni$_.JMethodIDPtr, _$string.pointer, _$arrayList.pointer).check(); + } + + static final _id_putParcelable = _class.instanceMethodId( + r'putParcelable', + r'(Ljava/lang/String;Landroid/os/Parcelable;)V', + ); + + static final _putParcelable = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putParcelable(java.lang.String string, android.os.Parcelable parcelable)` + void putParcelable(jni$_.JString? string, jni$_.JObject? parcelable, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$parcelable = parcelable?.reference ?? jni$_.jNullReference; + _putParcelable(reference.pointer, _id_putParcelable as jni$_.JMethodIDPtr, _$string.pointer, _$parcelable.pointer).check(); + } + + static final _id_putParcelableArray = _class.instanceMethodId( + r'putParcelableArray', + r'(Ljava/lang/String;[Landroid/os/Parcelable;)V', + ); + + static final _putParcelableArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putParcelableArray(java.lang.String string, android.os.Parcelable[] parcelables)` + void putParcelableArray(jni$_.JString? string, jni$_.JArray? parcelables, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$parcelables = parcelables?.reference ?? jni$_.jNullReference; + _putParcelableArray(reference.pointer, _id_putParcelableArray as jni$_.JMethodIDPtr, _$string.pointer, _$parcelables.pointer).check(); + } + + static final _id_putParcelableArrayList = _class.instanceMethodId( + r'putParcelableArrayList', + r'(Ljava/lang/String;Ljava/util/ArrayList;)V', + ); + + static final _putParcelableArrayList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putParcelableArrayList(java.lang.String string, java.util.ArrayList arrayList)` + void putParcelableArrayList(jni$_.JString? string, jni$_.JObject? arrayList, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + _putParcelableArrayList(reference.pointer, _id_putParcelableArrayList as jni$_.JMethodIDPtr, _$string.pointer, _$arrayList.pointer).check(); + } + + static final _id_putSerializable = _class.instanceMethodId( + r'putSerializable', + r'(Ljava/lang/String;Ljava/io/Serializable;)V', + ); + + static final _putSerializable = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putSerializable(java.lang.String string, java.io.Serializable serializable)` + void putSerializable(jni$_.JString? string, jni$_.JObject? serializable, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$serializable = serializable?.reference ?? jni$_.jNullReference; + _putSerializable(reference.pointer, _id_putSerializable as jni$_.JMethodIDPtr, _$string.pointer, _$serializable.pointer).check(); + } + + static final _id_putShort = _class.instanceMethodId( + r'putShort', + r'(Ljava/lang/String;S)V', + ); + + static final _putShort = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public void putShort(java.lang.String string, short s)` + void putShort(jni$_.JString? string, int s, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + _putShort(reference.pointer, _id_putShort as jni$_.JMethodIDPtr, _$string.pointer, s).check(); + } + + static final _id_putShortArray = _class.instanceMethodId( + r'putShortArray', + r'(Ljava/lang/String;[S)V', + ); + + static final _putShortArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putShortArray(java.lang.String string, short[] ss)` + void putShortArray(jni$_.JString? string, jni$_.JShortArray? ss, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$ss = ss?.reference ?? jni$_.jNullReference; + _putShortArray(reference.pointer, _id_putShortArray as jni$_.JMethodIDPtr, _$string.pointer, _$ss.pointer).check(); + } + + static final _id_putSize = _class.instanceMethodId( + r'putSize', + r'(Ljava/lang/String;Landroid/util/Size;)V', + ); + + static final _putSize = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putSize(java.lang.String string, android.util.Size size)` + void putSize(jni$_.JString? string, jni$_.JObject? size, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$size = size?.reference ?? jni$_.jNullReference; + _putSize(reference.pointer, _id_putSize as jni$_.JMethodIDPtr, _$string.pointer, _$size.pointer).check(); + } + + static final _id_putSizeF = _class.instanceMethodId( + r'putSizeF', + r'(Ljava/lang/String;Landroid/util/SizeF;)V', + ); + + static final _putSizeF = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putSizeF(java.lang.String string, android.util.SizeF sizeF)` + void putSizeF(jni$_.JString? string, jni$_.JObject? sizeF, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$sizeF = sizeF?.reference ?? jni$_.jNullReference; + _putSizeF(reference.pointer, _id_putSizeF as jni$_.JMethodIDPtr, _$string.pointer, _$sizeF.pointer).check(); + } + + static final _id_putSparseParcelableArray = _class.instanceMethodId( + r'putSparseParcelableArray', + r'(Ljava/lang/String;Landroid/util/SparseArray;)V', + ); + + static final _putSparseParcelableArray = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putSparseParcelableArray(java.lang.String string, android.util.SparseArray sparseArray)` + void putSparseParcelableArray(jni$_.JString? string, jni$_.JObject? sparseArray, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$sparseArray = sparseArray?.reference ?? jni$_.jNullReference; + _putSparseParcelableArray(reference.pointer, _id_putSparseParcelableArray as jni$_.JMethodIDPtr, _$string.pointer, _$sparseArray.pointer).check(); + } + + static final _id_putStringArrayList = _class.instanceMethodId( + r'putStringArrayList', + r'(Ljava/lang/String;Ljava/util/ArrayList;)V', + ); + + static final _putStringArrayList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void putStringArrayList(java.lang.String string, java.util.ArrayList arrayList)` + void putStringArrayList(jni$_.JString? string, jni$_.JObject? arrayList, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + _putStringArrayList(reference.pointer, _id_putStringArrayList as jni$_.JMethodIDPtr, _$string.pointer, _$arrayList.pointer).check(); + } + + static final _id_readFromParcel = _class.instanceMethodId( + r'readFromParcel', + r'(Landroid/os/Parcel;)V', + ); + + static final _readFromParcel = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void readFromParcel(android.os.Parcel parcel)` + void readFromParcel(jni$_.JObject? parcel, ){ + + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _readFromParcel(reference.pointer, _id_readFromParcel as jni$_.JMethodIDPtr, _$parcel.pointer).check(); + } + + static final _id_remove = _class.instanceMethodId( + r'remove', + r'(Ljava/lang/String;)V', + ); + + static final _remove = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void remove(java.lang.String string)` + void remove(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + _remove(reference.pointer, _id_remove as jni$_.JMethodIDPtr, _$string.pointer).check(); + } + + static final _id_setClassLoader = _class.instanceMethodId( + r'setClassLoader', + r'(Ljava/lang/ClassLoader;)V', + ); + + static final _setClassLoader = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void setClassLoader(java.lang.ClassLoader classLoader)` + void setClassLoader(jni$_.JObject? classLoader, ){ + + final _$classLoader = classLoader?.reference ?? jni$_.jNullReference; + _setClassLoader(reference.pointer, _id_setClassLoader as jni$_.JMethodIDPtr, _$classLoader.pointer).check(); + } + + static final _id_toString$1 = _class.instanceMethodId( + r'toString', + r'()Ljava/lang/String;', + ); + + static final _toString$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String toString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toString$1(){ + + + return _toString$1(reference.pointer, _id_toString$1 as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_writeToParcel = _class.instanceMethodId( + r'writeToParcel', + r'(Landroid/os/Parcel;I)V', + ); + + static final _writeToParcel = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public void writeToParcel(android.os.Parcel parcel, int i)` + void writeToParcel(jni$_.JObject? parcel, int i, ){ + + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _writeToParcel(reference.pointer, _id_writeToParcel as jni$_.JMethodIDPtr, _$parcel.pointer, i).check(); + } + +} +final class $Bundle$NullableType$ extends jni$_.JType { + + + @jni$_.internal + const $Bundle$NullableType$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/os/Bundle;'; + + @jni$_.internal + @core$_.override + Bundle? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Bundle.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Bundle$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Bundle$NullableType$) && + other is $Bundle$NullableType$; + } +} + +final class $Bundle$Type$ extends jni$_.JType { + + + @jni$_.internal + const $Bundle$Type$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/os/Bundle;'; + + @jni$_.internal + @core$_.override + Bundle fromReference(jni$_.JReference reference) => + Bundle.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Bundle$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Bundle$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Bundle$Type$) && + other is $Bundle$Type$; + } +} + +/// from: `android.content.Intent$FilterComparison` +class Intent$FilterComparison extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + + + @jni$_.internal + Intent$FilterComparison.fromReference( + + jni$_.JReference reference, + ) : + $type = type, + super.fromReference( + + reference + ); + + static final _class = jni$_.JClass.forName(r'android/content/Intent$FilterComparison'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Intent$FilterComparison$NullableType$(); + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Intent$FilterComparison$Type$(); + static final _id_new$ = _class.constructorId( + r'(Landroid/content/Intent;)V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void (android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent$FilterComparison(Intent? intent, ) { + + final _$intent = intent?.reference ?? jni$_.jNullReference; + return Intent$FilterComparison.fromReference( + + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr, _$intent.pointer).reference + ); + } + + static final _id_equals = _class.instanceMethodId( + r'equals', + r'(Ljava/lang/Object;)Z', + ); + + static final _equals = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public boolean equals(java.lang.Object object)` + bool equals(jni$_.JObject? object, ){ + + final _$object = object?.reference ?? jni$_.jNullReference; + return _equals(reference.pointer, _id_equals as jni$_.JMethodIDPtr, _$object.pointer).boolean; + } + + static final _id_getIntent = _class.instanceMethodId( + r'getIntent', + r'()Landroid/content/Intent;', + ); + + static final _getIntent = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.content.Intent getIntent()` + /// The returned object must be released after use, by calling the [release] method. + Intent? getIntent(){ + + + return _getIntent(reference.pointer, _id_getIntent as jni$_.JMethodIDPtr).object(const $Intent$NullableType$()); + } + + static final _id_hashCode$1 = _class.instanceMethodId( + r'hashCode', + r'()I', + ); + + static final _hashCode$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int hashCode()` + int hashCode$1(){ + + + return _hashCode$1(reference.pointer, _id_hashCode$1 as jni$_.JMethodIDPtr).integer; + } + +} +final class $Intent$FilterComparison$NullableType$ extends jni$_.JType { + + + @jni$_.internal + const $Intent$FilterComparison$NullableType$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent$FilterComparison;'; + + @jni$_.internal + @core$_.override + Intent$FilterComparison? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Intent$FilterComparison.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$FilterComparison$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$FilterComparison$NullableType$) && + other is $Intent$FilterComparison$NullableType$; + } +} + +final class $Intent$FilterComparison$Type$ extends jni$_.JType { + + + @jni$_.internal + const $Intent$FilterComparison$Type$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent$FilterComparison;'; + + @jni$_.internal + @core$_.override + Intent$FilterComparison fromReference(jni$_.JReference reference) => + Intent$FilterComparison.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Intent$FilterComparison$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$FilterComparison$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$FilterComparison$Type$) && + other is $Intent$FilterComparison$Type$; + } +} + +/// from: `android.content.Intent$ShortcutIconResource` +class Intent$ShortcutIconResource extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + + + @jni$_.internal + Intent$ShortcutIconResource.fromReference( + + jni$_.JReference reference, + ) : + $type = type, + super.fromReference( + + reference + ); + + static final _class = jni$_.JClass.forName(r'android/content/Intent$ShortcutIconResource'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Intent$ShortcutIconResource$NullableType$(); + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Intent$ShortcutIconResource$Type$(); + static final _id_CREATOR = + _class.staticFieldId( + r'CREATOR', + r'Landroid/os/Parcelable$Creator;', + ); + /// from: `static public final android.os.Parcelable$Creator CREATOR` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JObject? get CREATOR => _id_CREATOR.get(_class, const jni$_.$JObject$NullableType$()); + + static final _id_packageName = + _class.instanceFieldId( + r'packageName', + r'Ljava/lang/String;', + ); + /// from: `public java.lang.String packageName` + /// The returned object must be released after use, by calling the [release] method. +jni$_.JString? get packageName => _id_packageName.get(this, const jni$_.$JString$NullableType$()); + + /// from: `public java.lang.String packageName` + /// The returned object must be released after use, by calling the [release] method. +set packageName(jni$_.JString? value) => _id_packageName.set(this, const jni$_.$JString$NullableType$(), value); + + static final _id_resourceName = + _class.instanceFieldId( + r'resourceName', + r'Ljava/lang/String;', + ); + /// from: `public java.lang.String resourceName` + /// The returned object must be released after use, by calling the [release] method. +jni$_.JString? get resourceName => _id_resourceName.get(this, const jni$_.$JString$NullableType$()); + + /// from: `public java.lang.String resourceName` + /// The returned object must be released after use, by calling the [release] method. +set resourceName(jni$_.JString? value) => _id_resourceName.set(this, const jni$_.$JString$NullableType$(), value); + + static final _id_new$ = _class.constructorId( + r'()V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory Intent$ShortcutIconResource() { + + + return Intent$ShortcutIconResource.fromReference( + + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference + ); + } + + static final _id_describeContents = _class.instanceMethodId( + r'describeContents', + r'()I', + ); + + static final _describeContents = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int describeContents()` + int describeContents(){ + + + return _describeContents(reference.pointer, _id_describeContents as jni$_.JMethodIDPtr).integer; + } + + static final _id_fromContext = _class.staticMethodId( + r'fromContext', + r'(Landroid/content/Context;I)Landroid/content/Intent$ShortcutIconResource;', + ); + + static final _fromContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `static public android.content.Intent$ShortcutIconResource fromContext(android.content.Context context, int i)` + /// The returned object must be released after use, by calling the [release] method. + static Intent$ShortcutIconResource? fromContext(Context? context, int i, ){ + + final _$context = context?.reference ?? jni$_.jNullReference; + return _fromContext(_class.reference.pointer, _id_fromContext as jni$_.JMethodIDPtr, _$context.pointer, i).object(const $Intent$ShortcutIconResource$NullableType$()); + } + + static final _id_toString$1 = _class.instanceMethodId( + r'toString', + r'()Ljava/lang/String;', + ); + + static final _toString$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String toString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toString$1(){ + + + return _toString$1(reference.pointer, _id_toString$1 as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_writeToParcel = _class.instanceMethodId( + r'writeToParcel', + r'(Landroid/os/Parcel;I)V', + ); + + static final _writeToParcel = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public void writeToParcel(android.os.Parcel parcel, int i)` + void writeToParcel(jni$_.JObject? parcel, int i, ){ + + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _writeToParcel(reference.pointer, _id_writeToParcel as jni$_.JMethodIDPtr, _$parcel.pointer, i).check(); + } + +} +final class $Intent$ShortcutIconResource$NullableType$ extends jni$_.JType { + + + @jni$_.internal + const $Intent$ShortcutIconResource$NullableType$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent$ShortcutIconResource;'; + + @jni$_.internal + @core$_.override + Intent$ShortcutIconResource? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Intent$ShortcutIconResource.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$ShortcutIconResource$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$ShortcutIconResource$NullableType$) && + other is $Intent$ShortcutIconResource$NullableType$; + } +} + +final class $Intent$ShortcutIconResource$Type$ extends jni$_.JType { + + + @jni$_.internal + const $Intent$ShortcutIconResource$Type$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent$ShortcutIconResource;'; + + @jni$_.internal + @core$_.override + Intent$ShortcutIconResource fromReference(jni$_.JReference reference) => + Intent$ShortcutIconResource.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Intent$ShortcutIconResource$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$ShortcutIconResource$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$ShortcutIconResource$Type$) && + other is $Intent$ShortcutIconResource$Type$; + } +} + +/// from: `android.content.Intent` +class Intent extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + + + @jni$_.internal + Intent.fromReference( + + jni$_.JReference reference, + ) : + $type = type, + super.fromReference( + + reference + ); + + static final _class = jni$_.JClass.forName(r'android/content/Intent'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Intent$NullableType$(); + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Intent$Type$(); + static final _id_ACTION_AIRPLANE_MODE_CHANGED = + _class.staticFieldId( + r'ACTION_AIRPLANE_MODE_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_AIRPLANE_MODE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_AIRPLANE_MODE_CHANGED => _id_ACTION_AIRPLANE_MODE_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_ALL_APPS = + _class.staticFieldId( + r'ACTION_ALL_APPS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_ALL_APPS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_ALL_APPS => _id_ACTION_ALL_APPS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_ANSWER = + _class.staticFieldId( + r'ACTION_ANSWER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_ANSWER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_ANSWER => _id_ACTION_ANSWER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_APPLICATION_LOCALE_CHANGED = + _class.staticFieldId( + r'ACTION_APPLICATION_LOCALE_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_APPLICATION_LOCALE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_APPLICATION_LOCALE_CHANGED => _id_ACTION_APPLICATION_LOCALE_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_APPLICATION_PREFERENCES = + _class.staticFieldId( + r'ACTION_APPLICATION_PREFERENCES', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_APPLICATION_PREFERENCES` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_APPLICATION_PREFERENCES => _id_ACTION_APPLICATION_PREFERENCES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_APPLICATION_RESTRICTIONS_CHANGED = + _class.staticFieldId( + r'ACTION_APPLICATION_RESTRICTIONS_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_APPLICATION_RESTRICTIONS_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_APPLICATION_RESTRICTIONS_CHANGED => _id_ACTION_APPLICATION_RESTRICTIONS_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_APP_ERROR = + _class.staticFieldId( + r'ACTION_APP_ERROR', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_APP_ERROR` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_APP_ERROR => _id_ACTION_APP_ERROR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_ASSIST = + _class.staticFieldId( + r'ACTION_ASSIST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_ASSIST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_ASSIST => _id_ACTION_ASSIST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_ATTACH_DATA = + _class.staticFieldId( + r'ACTION_ATTACH_DATA', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_ATTACH_DATA` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_ATTACH_DATA => _id_ACTION_ATTACH_DATA.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_AUTO_REVOKE_PERMISSIONS = + _class.staticFieldId( + r'ACTION_AUTO_REVOKE_PERMISSIONS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_AUTO_REVOKE_PERMISSIONS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_AUTO_REVOKE_PERMISSIONS => _id_ACTION_AUTO_REVOKE_PERMISSIONS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_BATTERY_CHANGED = + _class.staticFieldId( + r'ACTION_BATTERY_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_BATTERY_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_BATTERY_CHANGED => _id_ACTION_BATTERY_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_BATTERY_LOW = + _class.staticFieldId( + r'ACTION_BATTERY_LOW', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_BATTERY_LOW` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_BATTERY_LOW => _id_ACTION_BATTERY_LOW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_BATTERY_OKAY = + _class.staticFieldId( + r'ACTION_BATTERY_OKAY', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_BATTERY_OKAY` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_BATTERY_OKAY => _id_ACTION_BATTERY_OKAY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_BOOT_COMPLETED = + _class.staticFieldId( + r'ACTION_BOOT_COMPLETED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_BOOT_COMPLETED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_BOOT_COMPLETED => _id_ACTION_BOOT_COMPLETED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_BUG_REPORT = + _class.staticFieldId( + r'ACTION_BUG_REPORT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_BUG_REPORT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_BUG_REPORT => _id_ACTION_BUG_REPORT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CALL = + _class.staticFieldId( + r'ACTION_CALL', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CALL` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CALL => _id_ACTION_CALL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CALL_BUTTON = + _class.staticFieldId( + r'ACTION_CALL_BUTTON', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CALL_BUTTON` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CALL_BUTTON => _id_ACTION_CALL_BUTTON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CAMERA_BUTTON = + _class.staticFieldId( + r'ACTION_CAMERA_BUTTON', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CAMERA_BUTTON` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CAMERA_BUTTON => _id_ACTION_CAMERA_BUTTON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CARRIER_SETUP = + _class.staticFieldId( + r'ACTION_CARRIER_SETUP', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CARRIER_SETUP` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CARRIER_SETUP => _id_ACTION_CARRIER_SETUP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CHOOSER = + _class.staticFieldId( + r'ACTION_CHOOSER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CHOOSER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CHOOSER => _id_ACTION_CHOOSER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CLOSE_SYSTEM_DIALOGS = + _class.staticFieldId( + r'ACTION_CLOSE_SYSTEM_DIALOGS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CLOSE_SYSTEM_DIALOGS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CLOSE_SYSTEM_DIALOGS => _id_ACTION_CLOSE_SYSTEM_DIALOGS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CONFIGURATION_CHANGED = + _class.staticFieldId( + r'ACTION_CONFIGURATION_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CONFIGURATION_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CONFIGURATION_CHANGED => _id_ACTION_CONFIGURATION_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CREATE_DOCUMENT = + _class.staticFieldId( + r'ACTION_CREATE_DOCUMENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CREATE_DOCUMENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CREATE_DOCUMENT => _id_ACTION_CREATE_DOCUMENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CREATE_NOTE = + _class.staticFieldId( + r'ACTION_CREATE_NOTE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CREATE_NOTE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CREATE_NOTE => _id_ACTION_CREATE_NOTE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CREATE_REMINDER = + _class.staticFieldId( + r'ACTION_CREATE_REMINDER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CREATE_REMINDER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CREATE_REMINDER => _id_ACTION_CREATE_REMINDER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CREATE_SHORTCUT = + _class.staticFieldId( + r'ACTION_CREATE_SHORTCUT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_CREATE_SHORTCUT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_CREATE_SHORTCUT => _id_ACTION_CREATE_SHORTCUT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DATE_CHANGED = + _class.staticFieldId( + r'ACTION_DATE_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DATE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DATE_CHANGED => _id_ACTION_DATE_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DEFAULT = + _class.staticFieldId( + r'ACTION_DEFAULT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DEFAULT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DEFAULT => _id_ACTION_DEFAULT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DEFINE = + _class.staticFieldId( + r'ACTION_DEFINE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DEFINE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DEFINE => _id_ACTION_DEFINE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DELETE = + _class.staticFieldId( + r'ACTION_DELETE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DELETE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DELETE => _id_ACTION_DELETE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DEVICE_STORAGE_LOW = + _class.staticFieldId( + r'ACTION_DEVICE_STORAGE_LOW', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DEVICE_STORAGE_LOW` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DEVICE_STORAGE_LOW => _id_ACTION_DEVICE_STORAGE_LOW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DEVICE_STORAGE_OK = + _class.staticFieldId( + r'ACTION_DEVICE_STORAGE_OK', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DEVICE_STORAGE_OK` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DEVICE_STORAGE_OK => _id_ACTION_DEVICE_STORAGE_OK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DIAL = + _class.staticFieldId( + r'ACTION_DIAL', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DIAL` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DIAL => _id_ACTION_DIAL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DOCK_EVENT = + _class.staticFieldId( + r'ACTION_DOCK_EVENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DOCK_EVENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DOCK_EVENT => _id_ACTION_DOCK_EVENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DREAMING_STARTED = + _class.staticFieldId( + r'ACTION_DREAMING_STARTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DREAMING_STARTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DREAMING_STARTED => _id_ACTION_DREAMING_STARTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DREAMING_STOPPED = + _class.staticFieldId( + r'ACTION_DREAMING_STOPPED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_DREAMING_STOPPED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_DREAMING_STOPPED => _id_ACTION_DREAMING_STOPPED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_EDIT = + _class.staticFieldId( + r'ACTION_EDIT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_EDIT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_EDIT => _id_ACTION_EDIT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_EXTERNAL_APPLICATIONS_AVAILABLE = + _class.staticFieldId( + r'ACTION_EXTERNAL_APPLICATIONS_AVAILABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_EXTERNAL_APPLICATIONS_AVAILABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_EXTERNAL_APPLICATIONS_AVAILABLE => _id_ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE = + _class.staticFieldId( + r'ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE => _id_ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_FACTORY_TEST = + _class.staticFieldId( + r'ACTION_FACTORY_TEST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_FACTORY_TEST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_FACTORY_TEST => _id_ACTION_FACTORY_TEST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_GET_CONTENT = + _class.staticFieldId( + r'ACTION_GET_CONTENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_GET_CONTENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_GET_CONTENT => _id_ACTION_GET_CONTENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_GET_RESTRICTION_ENTRIES = + _class.staticFieldId( + r'ACTION_GET_RESTRICTION_ENTRIES', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_GET_RESTRICTION_ENTRIES` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_GET_RESTRICTION_ENTRIES => _id_ACTION_GET_RESTRICTION_ENTRIES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_GTALK_SERVICE_CONNECTED = + _class.staticFieldId( + r'ACTION_GTALK_SERVICE_CONNECTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_GTALK_SERVICE_CONNECTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_GTALK_SERVICE_CONNECTED => _id_ACTION_GTALK_SERVICE_CONNECTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_GTALK_SERVICE_DISCONNECTED = + _class.staticFieldId( + r'ACTION_GTALK_SERVICE_DISCONNECTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_GTALK_SERVICE_DISCONNECTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_GTALK_SERVICE_DISCONNECTED => _id_ACTION_GTALK_SERVICE_DISCONNECTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_HEADSET_PLUG = + _class.staticFieldId( + r'ACTION_HEADSET_PLUG', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_HEADSET_PLUG` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_HEADSET_PLUG => _id_ACTION_HEADSET_PLUG.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_INPUT_METHOD_CHANGED = + _class.staticFieldId( + r'ACTION_INPUT_METHOD_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_INPUT_METHOD_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_INPUT_METHOD_CHANGED => _id_ACTION_INPUT_METHOD_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_INSERT = + _class.staticFieldId( + r'ACTION_INSERT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_INSERT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_INSERT => _id_ACTION_INSERT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_INSERT_OR_EDIT = + _class.staticFieldId( + r'ACTION_INSERT_OR_EDIT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_INSERT_OR_EDIT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_INSERT_OR_EDIT => _id_ACTION_INSERT_OR_EDIT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_INSTALL_FAILURE = + _class.staticFieldId( + r'ACTION_INSTALL_FAILURE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_INSTALL_FAILURE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_INSTALL_FAILURE => _id_ACTION_INSTALL_FAILURE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_INSTALL_PACKAGE = + _class.staticFieldId( + r'ACTION_INSTALL_PACKAGE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_INSTALL_PACKAGE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_INSTALL_PACKAGE => _id_ACTION_INSTALL_PACKAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE = + _class.staticFieldId( + r'ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE => _id_ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_LOCALE_CHANGED = + _class.staticFieldId( + r'ACTION_LOCALE_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_LOCALE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_LOCALE_CHANGED => _id_ACTION_LOCALE_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_LOCKED_BOOT_COMPLETED = + _class.staticFieldId( + r'ACTION_LOCKED_BOOT_COMPLETED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_LOCKED_BOOT_COMPLETED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_LOCKED_BOOT_COMPLETED => _id_ACTION_LOCKED_BOOT_COMPLETED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MAIN = + _class.staticFieldId( + r'ACTION_MAIN', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MAIN` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MAIN => _id_ACTION_MAIN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MANAGED_PROFILE_ADDED = + _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_ADDED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_ADDED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MANAGED_PROFILE_ADDED => _id_ACTION_MANAGED_PROFILE_ADDED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MANAGED_PROFILE_AVAILABLE = + _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_AVAILABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_AVAILABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MANAGED_PROFILE_AVAILABLE => _id_ACTION_MANAGED_PROFILE_AVAILABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MANAGED_PROFILE_REMOVED = + _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_REMOVED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_REMOVED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MANAGED_PROFILE_REMOVED => _id_ACTION_MANAGED_PROFILE_REMOVED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MANAGED_PROFILE_UNAVAILABLE = + _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_UNAVAILABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_UNAVAILABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MANAGED_PROFILE_UNAVAILABLE => _id_ACTION_MANAGED_PROFILE_UNAVAILABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MANAGED_PROFILE_UNLOCKED = + _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_UNLOCKED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_UNLOCKED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MANAGED_PROFILE_UNLOCKED => _id_ACTION_MANAGED_PROFILE_UNLOCKED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MANAGE_NETWORK_USAGE = + _class.staticFieldId( + r'ACTION_MANAGE_NETWORK_USAGE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MANAGE_NETWORK_USAGE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MANAGE_NETWORK_USAGE => _id_ACTION_MANAGE_NETWORK_USAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MANAGE_PACKAGE_STORAGE = + _class.staticFieldId( + r'ACTION_MANAGE_PACKAGE_STORAGE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MANAGE_PACKAGE_STORAGE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MANAGE_PACKAGE_STORAGE => _id_ACTION_MANAGE_PACKAGE_STORAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MANAGE_UNUSED_APPS = + _class.staticFieldId( + r'ACTION_MANAGE_UNUSED_APPS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MANAGE_UNUSED_APPS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MANAGE_UNUSED_APPS => _id_ACTION_MANAGE_UNUSED_APPS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_BAD_REMOVAL = + _class.staticFieldId( + r'ACTION_MEDIA_BAD_REMOVAL', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_BAD_REMOVAL` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_BAD_REMOVAL => _id_ACTION_MEDIA_BAD_REMOVAL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_BUTTON = + _class.staticFieldId( + r'ACTION_MEDIA_BUTTON', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_BUTTON` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_BUTTON => _id_ACTION_MEDIA_BUTTON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_CHECKING = + _class.staticFieldId( + r'ACTION_MEDIA_CHECKING', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_CHECKING` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_CHECKING => _id_ACTION_MEDIA_CHECKING.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_EJECT = + _class.staticFieldId( + r'ACTION_MEDIA_EJECT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_EJECT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_EJECT => _id_ACTION_MEDIA_EJECT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_MOUNTED = + _class.staticFieldId( + r'ACTION_MEDIA_MOUNTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_MOUNTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_MOUNTED => _id_ACTION_MEDIA_MOUNTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_NOFS = + _class.staticFieldId( + r'ACTION_MEDIA_NOFS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_NOFS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_NOFS => _id_ACTION_MEDIA_NOFS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_REMOVED = + _class.staticFieldId( + r'ACTION_MEDIA_REMOVED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_REMOVED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_REMOVED => _id_ACTION_MEDIA_REMOVED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_SCANNER_FINISHED = + _class.staticFieldId( + r'ACTION_MEDIA_SCANNER_FINISHED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_SCANNER_FINISHED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_SCANNER_FINISHED => _id_ACTION_MEDIA_SCANNER_FINISHED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_SCANNER_SCAN_FILE = + _class.staticFieldId( + r'ACTION_MEDIA_SCANNER_SCAN_FILE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_SCANNER_SCAN_FILE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_SCANNER_SCAN_FILE => _id_ACTION_MEDIA_SCANNER_SCAN_FILE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_SCANNER_STARTED = + _class.staticFieldId( + r'ACTION_MEDIA_SCANNER_STARTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_SCANNER_STARTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_SCANNER_STARTED => _id_ACTION_MEDIA_SCANNER_STARTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_SHARED = + _class.staticFieldId( + r'ACTION_MEDIA_SHARED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_SHARED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_SHARED => _id_ACTION_MEDIA_SHARED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_UNMOUNTABLE = + _class.staticFieldId( + r'ACTION_MEDIA_UNMOUNTABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_UNMOUNTABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_UNMOUNTABLE => _id_ACTION_MEDIA_UNMOUNTABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_UNMOUNTED = + _class.staticFieldId( + r'ACTION_MEDIA_UNMOUNTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MEDIA_UNMOUNTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MEDIA_UNMOUNTED => _id_ACTION_MEDIA_UNMOUNTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MY_PACKAGE_REPLACED = + _class.staticFieldId( + r'ACTION_MY_PACKAGE_REPLACED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MY_PACKAGE_REPLACED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MY_PACKAGE_REPLACED => _id_ACTION_MY_PACKAGE_REPLACED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MY_PACKAGE_SUSPENDED = + _class.staticFieldId( + r'ACTION_MY_PACKAGE_SUSPENDED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MY_PACKAGE_SUSPENDED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MY_PACKAGE_SUSPENDED => _id_ACTION_MY_PACKAGE_SUSPENDED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MY_PACKAGE_UNSUSPENDED = + _class.staticFieldId( + r'ACTION_MY_PACKAGE_UNSUSPENDED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_MY_PACKAGE_UNSUSPENDED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_MY_PACKAGE_UNSUSPENDED => _id_ACTION_MY_PACKAGE_UNSUSPENDED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_NEW_OUTGOING_CALL = + _class.staticFieldId( + r'ACTION_NEW_OUTGOING_CALL', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_NEW_OUTGOING_CALL` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_NEW_OUTGOING_CALL => _id_ACTION_NEW_OUTGOING_CALL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_OPEN_DOCUMENT = + _class.staticFieldId( + r'ACTION_OPEN_DOCUMENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_OPEN_DOCUMENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_OPEN_DOCUMENT => _id_ACTION_OPEN_DOCUMENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_OPEN_DOCUMENT_TREE = + _class.staticFieldId( + r'ACTION_OPEN_DOCUMENT_TREE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_OPEN_DOCUMENT_TREE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_OPEN_DOCUMENT_TREE => _id_ACTION_OPEN_DOCUMENT_TREE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGES_SUSPENDED = + _class.staticFieldId( + r'ACTION_PACKAGES_SUSPENDED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGES_SUSPENDED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGES_SUSPENDED => _id_ACTION_PACKAGES_SUSPENDED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGES_UNSUSPENDED = + _class.staticFieldId( + r'ACTION_PACKAGES_UNSUSPENDED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGES_UNSUSPENDED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGES_UNSUSPENDED => _id_ACTION_PACKAGES_UNSUSPENDED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_ADDED = + _class.staticFieldId( + r'ACTION_PACKAGE_ADDED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_ADDED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_ADDED => _id_ACTION_PACKAGE_ADDED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_CHANGED = + _class.staticFieldId( + r'ACTION_PACKAGE_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_CHANGED => _id_ACTION_PACKAGE_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_DATA_CLEARED = + _class.staticFieldId( + r'ACTION_PACKAGE_DATA_CLEARED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_DATA_CLEARED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_DATA_CLEARED => _id_ACTION_PACKAGE_DATA_CLEARED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_FIRST_LAUNCH = + _class.staticFieldId( + r'ACTION_PACKAGE_FIRST_LAUNCH', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_FIRST_LAUNCH` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_FIRST_LAUNCH => _id_ACTION_PACKAGE_FIRST_LAUNCH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_FULLY_REMOVED = + _class.staticFieldId( + r'ACTION_PACKAGE_FULLY_REMOVED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_FULLY_REMOVED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_FULLY_REMOVED => _id_ACTION_PACKAGE_FULLY_REMOVED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_INSTALL = + _class.staticFieldId( + r'ACTION_PACKAGE_INSTALL', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_INSTALL` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_INSTALL => _id_ACTION_PACKAGE_INSTALL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_NEEDS_VERIFICATION = + _class.staticFieldId( + r'ACTION_PACKAGE_NEEDS_VERIFICATION', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_NEEDS_VERIFICATION` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_NEEDS_VERIFICATION => _id_ACTION_PACKAGE_NEEDS_VERIFICATION.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_REMOVED = + _class.staticFieldId( + r'ACTION_PACKAGE_REMOVED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_REMOVED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_REMOVED => _id_ACTION_PACKAGE_REMOVED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_REPLACED = + _class.staticFieldId( + r'ACTION_PACKAGE_REPLACED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_REPLACED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_REPLACED => _id_ACTION_PACKAGE_REPLACED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_RESTARTED = + _class.staticFieldId( + r'ACTION_PACKAGE_RESTARTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_RESTARTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_RESTARTED => _id_ACTION_PACKAGE_RESTARTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_UNSTOPPED = + _class.staticFieldId( + r'ACTION_PACKAGE_UNSTOPPED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_UNSTOPPED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_UNSTOPPED => _id_ACTION_PACKAGE_UNSTOPPED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_VERIFIED = + _class.staticFieldId( + r'ACTION_PACKAGE_VERIFIED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PACKAGE_VERIFIED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PACKAGE_VERIFIED => _id_ACTION_PACKAGE_VERIFIED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PASTE = + _class.staticFieldId( + r'ACTION_PASTE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PASTE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PASTE => _id_ACTION_PASTE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PICK = + _class.staticFieldId( + r'ACTION_PICK', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PICK` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PICK => _id_ACTION_PICK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PICK_ACTIVITY = + _class.staticFieldId( + r'ACTION_PICK_ACTIVITY', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PICK_ACTIVITY` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PICK_ACTIVITY => _id_ACTION_PICK_ACTIVITY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_POWER_CONNECTED = + _class.staticFieldId( + r'ACTION_POWER_CONNECTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_POWER_CONNECTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_POWER_CONNECTED => _id_ACTION_POWER_CONNECTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_POWER_DISCONNECTED = + _class.staticFieldId( + r'ACTION_POWER_DISCONNECTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_POWER_DISCONNECTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_POWER_DISCONNECTED => _id_ACTION_POWER_DISCONNECTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_POWER_USAGE_SUMMARY = + _class.staticFieldId( + r'ACTION_POWER_USAGE_SUMMARY', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_POWER_USAGE_SUMMARY` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_POWER_USAGE_SUMMARY => _id_ACTION_POWER_USAGE_SUMMARY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROCESS_TEXT = + _class.staticFieldId( + r'ACTION_PROCESS_TEXT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PROCESS_TEXT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PROCESS_TEXT => _id_ACTION_PROCESS_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROFILE_ACCESSIBLE = + _class.staticFieldId( + r'ACTION_PROFILE_ACCESSIBLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PROFILE_ACCESSIBLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PROFILE_ACCESSIBLE => _id_ACTION_PROFILE_ACCESSIBLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROFILE_ADDED = + _class.staticFieldId( + r'ACTION_PROFILE_ADDED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PROFILE_ADDED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PROFILE_ADDED => _id_ACTION_PROFILE_ADDED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROFILE_AVAILABLE = + _class.staticFieldId( + r'ACTION_PROFILE_AVAILABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PROFILE_AVAILABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PROFILE_AVAILABLE => _id_ACTION_PROFILE_AVAILABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROFILE_INACCESSIBLE = + _class.staticFieldId( + r'ACTION_PROFILE_INACCESSIBLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PROFILE_INACCESSIBLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PROFILE_INACCESSIBLE => _id_ACTION_PROFILE_INACCESSIBLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROFILE_REMOVED = + _class.staticFieldId( + r'ACTION_PROFILE_REMOVED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PROFILE_REMOVED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PROFILE_REMOVED => _id_ACTION_PROFILE_REMOVED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROFILE_UNAVAILABLE = + _class.staticFieldId( + r'ACTION_PROFILE_UNAVAILABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PROFILE_UNAVAILABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PROFILE_UNAVAILABLE => _id_ACTION_PROFILE_UNAVAILABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROVIDER_CHANGED = + _class.staticFieldId( + r'ACTION_PROVIDER_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_PROVIDER_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_PROVIDER_CHANGED => _id_ACTION_PROVIDER_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_QUICK_CLOCK = + _class.staticFieldId( + r'ACTION_QUICK_CLOCK', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_QUICK_CLOCK` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_QUICK_CLOCK => _id_ACTION_QUICK_CLOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_QUICK_VIEW = + _class.staticFieldId( + r'ACTION_QUICK_VIEW', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_QUICK_VIEW` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_QUICK_VIEW => _id_ACTION_QUICK_VIEW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_REBOOT = + _class.staticFieldId( + r'ACTION_REBOOT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_REBOOT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_REBOOT => _id_ACTION_REBOOT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_RUN = + _class.staticFieldId( + r'ACTION_RUN', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_RUN` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_RUN => _id_ACTION_RUN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SAFETY_CENTER = + _class.staticFieldId( + r'ACTION_SAFETY_CENTER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SAFETY_CENTER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SAFETY_CENTER => _id_ACTION_SAFETY_CENTER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SCREEN_OFF = + _class.staticFieldId( + r'ACTION_SCREEN_OFF', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SCREEN_OFF` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SCREEN_OFF => _id_ACTION_SCREEN_OFF.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SCREEN_ON = + _class.staticFieldId( + r'ACTION_SCREEN_ON', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SCREEN_ON` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SCREEN_ON => _id_ACTION_SCREEN_ON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SEARCH = + _class.staticFieldId( + r'ACTION_SEARCH', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SEARCH` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SEARCH => _id_ACTION_SEARCH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SEARCH_LONG_PRESS = + _class.staticFieldId( + r'ACTION_SEARCH_LONG_PRESS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SEARCH_LONG_PRESS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SEARCH_LONG_PRESS => _id_ACTION_SEARCH_LONG_PRESS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SEND = + _class.staticFieldId( + r'ACTION_SEND', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SEND` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SEND => _id_ACTION_SEND.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SENDTO = + _class.staticFieldId( + r'ACTION_SENDTO', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SENDTO` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SENDTO => _id_ACTION_SENDTO.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SEND_MULTIPLE = + _class.staticFieldId( + r'ACTION_SEND_MULTIPLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SEND_MULTIPLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SEND_MULTIPLE => _id_ACTION_SEND_MULTIPLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SET_WALLPAPER = + _class.staticFieldId( + r'ACTION_SET_WALLPAPER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SET_WALLPAPER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SET_WALLPAPER => _id_ACTION_SET_WALLPAPER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SHOW_APP_INFO = + _class.staticFieldId( + r'ACTION_SHOW_APP_INFO', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SHOW_APP_INFO` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SHOW_APP_INFO => _id_ACTION_SHOW_APP_INFO.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SHOW_WORK_APPS = + _class.staticFieldId( + r'ACTION_SHOW_WORK_APPS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SHOW_WORK_APPS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SHOW_WORK_APPS => _id_ACTION_SHOW_WORK_APPS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SHUTDOWN = + _class.staticFieldId( + r'ACTION_SHUTDOWN', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SHUTDOWN` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SHUTDOWN => _id_ACTION_SHUTDOWN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SYNC = + _class.staticFieldId( + r'ACTION_SYNC', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SYNC` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SYNC => _id_ACTION_SYNC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SYSTEM_TUTORIAL = + _class.staticFieldId( + r'ACTION_SYSTEM_TUTORIAL', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_SYSTEM_TUTORIAL` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_SYSTEM_TUTORIAL => _id_ACTION_SYSTEM_TUTORIAL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_TIMEZONE_CHANGED = + _class.staticFieldId( + r'ACTION_TIMEZONE_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_TIMEZONE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_TIMEZONE_CHANGED => _id_ACTION_TIMEZONE_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_TIME_CHANGED = + _class.staticFieldId( + r'ACTION_TIME_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_TIME_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_TIME_CHANGED => _id_ACTION_TIME_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_TIME_TICK = + _class.staticFieldId( + r'ACTION_TIME_TICK', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_TIME_TICK` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_TIME_TICK => _id_ACTION_TIME_TICK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_TRANSLATE = + _class.staticFieldId( + r'ACTION_TRANSLATE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_TRANSLATE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_TRANSLATE => _id_ACTION_TRANSLATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_UID_REMOVED = + _class.staticFieldId( + r'ACTION_UID_REMOVED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_UID_REMOVED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_UID_REMOVED => _id_ACTION_UID_REMOVED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_UMS_CONNECTED = + _class.staticFieldId( + r'ACTION_UMS_CONNECTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_UMS_CONNECTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_UMS_CONNECTED => _id_ACTION_UMS_CONNECTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_UMS_DISCONNECTED = + _class.staticFieldId( + r'ACTION_UMS_DISCONNECTED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_UMS_DISCONNECTED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_UMS_DISCONNECTED => _id_ACTION_UMS_DISCONNECTED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_UNARCHIVE_PACKAGE = + _class.staticFieldId( + r'ACTION_UNARCHIVE_PACKAGE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_UNARCHIVE_PACKAGE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_UNARCHIVE_PACKAGE => _id_ACTION_UNARCHIVE_PACKAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_UNINSTALL_PACKAGE = + _class.staticFieldId( + r'ACTION_UNINSTALL_PACKAGE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_UNINSTALL_PACKAGE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_UNINSTALL_PACKAGE => _id_ACTION_UNINSTALL_PACKAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_USER_BACKGROUND = + _class.staticFieldId( + r'ACTION_USER_BACKGROUND', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_USER_BACKGROUND` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_USER_BACKGROUND => _id_ACTION_USER_BACKGROUND.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_USER_FOREGROUND = + _class.staticFieldId( + r'ACTION_USER_FOREGROUND', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_USER_FOREGROUND` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_USER_FOREGROUND => _id_ACTION_USER_FOREGROUND.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_USER_INITIALIZE = + _class.staticFieldId( + r'ACTION_USER_INITIALIZE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_USER_INITIALIZE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_USER_INITIALIZE => _id_ACTION_USER_INITIALIZE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_USER_PRESENT = + _class.staticFieldId( + r'ACTION_USER_PRESENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_USER_PRESENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_USER_PRESENT => _id_ACTION_USER_PRESENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_USER_UNLOCKED = + _class.staticFieldId( + r'ACTION_USER_UNLOCKED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_USER_UNLOCKED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_USER_UNLOCKED => _id_ACTION_USER_UNLOCKED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_VIEW = + _class.staticFieldId( + r'ACTION_VIEW', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_VIEW` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_VIEW => _id_ACTION_VIEW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_VIEW_LOCUS = + _class.staticFieldId( + r'ACTION_VIEW_LOCUS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_VIEW_LOCUS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_VIEW_LOCUS => _id_ACTION_VIEW_LOCUS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_VIEW_PERMISSION_USAGE = + _class.staticFieldId( + r'ACTION_VIEW_PERMISSION_USAGE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_VIEW_PERMISSION_USAGE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_VIEW_PERMISSION_USAGE => _id_ACTION_VIEW_PERMISSION_USAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD = + _class.staticFieldId( + r'ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD => _id_ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_VOICE_COMMAND = + _class.staticFieldId( + r'ACTION_VOICE_COMMAND', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_VOICE_COMMAND` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_VOICE_COMMAND => _id_ACTION_VOICE_COMMAND.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_WALLPAPER_CHANGED = + _class.staticFieldId( + r'ACTION_WALLPAPER_CHANGED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_WALLPAPER_CHANGED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_WALLPAPER_CHANGED => _id_ACTION_WALLPAPER_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_WEB_SEARCH = + _class.staticFieldId( + r'ACTION_WEB_SEARCH', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTION_WEB_SEARCH` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTION_WEB_SEARCH => _id_ACTION_WEB_SEARCH.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN` + static const CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN = 4; + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_FAILED` + static const CAPTURE_CONTENT_FOR_NOTE_FAILED = 1; + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_SUCCESS` + static const CAPTURE_CONTENT_FOR_NOTE_SUCCESS = 0; + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED` + static const CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED = 2; + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED` + static const CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED = 3; + static final _id_CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET = + _class.staticFieldId( + r'CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET => _id_CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_ALTERNATIVE = + _class.staticFieldId( + r'CATEGORY_ALTERNATIVE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_ALTERNATIVE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_ALTERNATIVE => _id_CATEGORY_ALTERNATIVE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_BROWSER = + _class.staticFieldId( + r'CATEGORY_APP_BROWSER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_BROWSER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_BROWSER => _id_CATEGORY_APP_BROWSER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_CALCULATOR = + _class.staticFieldId( + r'CATEGORY_APP_CALCULATOR', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_CALCULATOR` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_CALCULATOR => _id_CATEGORY_APP_CALCULATOR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_CALENDAR = + _class.staticFieldId( + r'CATEGORY_APP_CALENDAR', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_CALENDAR` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_CALENDAR => _id_CATEGORY_APP_CALENDAR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_CONTACTS = + _class.staticFieldId( + r'CATEGORY_APP_CONTACTS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_CONTACTS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_CONTACTS => _id_CATEGORY_APP_CONTACTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_EMAIL = + _class.staticFieldId( + r'CATEGORY_APP_EMAIL', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_EMAIL` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_EMAIL => _id_CATEGORY_APP_EMAIL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_FILES = + _class.staticFieldId( + r'CATEGORY_APP_FILES', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_FILES` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_FILES => _id_CATEGORY_APP_FILES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_FITNESS = + _class.staticFieldId( + r'CATEGORY_APP_FITNESS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_FITNESS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_FITNESS => _id_CATEGORY_APP_FITNESS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_GALLERY = + _class.staticFieldId( + r'CATEGORY_APP_GALLERY', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_GALLERY` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_GALLERY => _id_CATEGORY_APP_GALLERY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_MAPS = + _class.staticFieldId( + r'CATEGORY_APP_MAPS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_MAPS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_MAPS => _id_CATEGORY_APP_MAPS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_MARKET = + _class.staticFieldId( + r'CATEGORY_APP_MARKET', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_MARKET` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_MARKET => _id_CATEGORY_APP_MARKET.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_MESSAGING = + _class.staticFieldId( + r'CATEGORY_APP_MESSAGING', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_MESSAGING` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_MESSAGING => _id_CATEGORY_APP_MESSAGING.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_MUSIC = + _class.staticFieldId( + r'CATEGORY_APP_MUSIC', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_MUSIC` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_MUSIC => _id_CATEGORY_APP_MUSIC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_WEATHER = + _class.staticFieldId( + r'CATEGORY_APP_WEATHER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_APP_WEATHER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_APP_WEATHER => _id_CATEGORY_APP_WEATHER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_BROWSABLE = + _class.staticFieldId( + r'CATEGORY_BROWSABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_BROWSABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_BROWSABLE => _id_CATEGORY_BROWSABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_CAR_DOCK = + _class.staticFieldId( + r'CATEGORY_CAR_DOCK', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_CAR_DOCK` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_CAR_DOCK => _id_CATEGORY_CAR_DOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_CAR_MODE = + _class.staticFieldId( + r'CATEGORY_CAR_MODE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_CAR_MODE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_CAR_MODE => _id_CATEGORY_CAR_MODE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_DEFAULT = + _class.staticFieldId( + r'CATEGORY_DEFAULT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_DEFAULT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_DEFAULT => _id_CATEGORY_DEFAULT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_DESK_DOCK = + _class.staticFieldId( + r'CATEGORY_DESK_DOCK', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_DESK_DOCK` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_DESK_DOCK => _id_CATEGORY_DESK_DOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_DEVELOPMENT_PREFERENCE = + _class.staticFieldId( + r'CATEGORY_DEVELOPMENT_PREFERENCE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_DEVELOPMENT_PREFERENCE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_DEVELOPMENT_PREFERENCE => _id_CATEGORY_DEVELOPMENT_PREFERENCE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_EMBED = + _class.staticFieldId( + r'CATEGORY_EMBED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_EMBED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_EMBED => _id_CATEGORY_EMBED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST = + _class.staticFieldId( + r'CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST => _id_CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_HE_DESK_DOCK = + _class.staticFieldId( + r'CATEGORY_HE_DESK_DOCK', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_HE_DESK_DOCK` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_HE_DESK_DOCK => _id_CATEGORY_HE_DESK_DOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_HOME = + _class.staticFieldId( + r'CATEGORY_HOME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_HOME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_HOME => _id_CATEGORY_HOME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_INFO = + _class.staticFieldId( + r'CATEGORY_INFO', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_INFO` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_INFO => _id_CATEGORY_INFO.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_LAUNCHER = + _class.staticFieldId( + r'CATEGORY_LAUNCHER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_LAUNCHER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_LAUNCHER => _id_CATEGORY_LAUNCHER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_LEANBACK_LAUNCHER = + _class.staticFieldId( + r'CATEGORY_LEANBACK_LAUNCHER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_LEANBACK_LAUNCHER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_LEANBACK_LAUNCHER => _id_CATEGORY_LEANBACK_LAUNCHER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_LE_DESK_DOCK = + _class.staticFieldId( + r'CATEGORY_LE_DESK_DOCK', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_LE_DESK_DOCK` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_LE_DESK_DOCK => _id_CATEGORY_LE_DESK_DOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_MONKEY = + _class.staticFieldId( + r'CATEGORY_MONKEY', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_MONKEY` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_MONKEY => _id_CATEGORY_MONKEY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_OPENABLE = + _class.staticFieldId( + r'CATEGORY_OPENABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_OPENABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_OPENABLE => _id_CATEGORY_OPENABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_PREFERENCE = + _class.staticFieldId( + r'CATEGORY_PREFERENCE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_PREFERENCE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_PREFERENCE => _id_CATEGORY_PREFERENCE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_SAMPLE_CODE = + _class.staticFieldId( + r'CATEGORY_SAMPLE_CODE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_SAMPLE_CODE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_SAMPLE_CODE => _id_CATEGORY_SAMPLE_CODE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_SECONDARY_HOME = + _class.staticFieldId( + r'CATEGORY_SECONDARY_HOME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_SECONDARY_HOME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_SECONDARY_HOME => _id_CATEGORY_SECONDARY_HOME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_SELECTED_ALTERNATIVE = + _class.staticFieldId( + r'CATEGORY_SELECTED_ALTERNATIVE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_SELECTED_ALTERNATIVE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_SELECTED_ALTERNATIVE => _id_CATEGORY_SELECTED_ALTERNATIVE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_TAB = + _class.staticFieldId( + r'CATEGORY_TAB', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_TAB` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_TAB => _id_CATEGORY_TAB.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_TEST = + _class.staticFieldId( + r'CATEGORY_TEST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_TEST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_TEST => _id_CATEGORY_TEST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_TYPED_OPENABLE = + _class.staticFieldId( + r'CATEGORY_TYPED_OPENABLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_TYPED_OPENABLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_TYPED_OPENABLE => _id_CATEGORY_TYPED_OPENABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_UNIT_TEST = + _class.staticFieldId( + r'CATEGORY_UNIT_TEST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_UNIT_TEST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_UNIT_TEST => _id_CATEGORY_UNIT_TEST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_VOICE = + _class.staticFieldId( + r'CATEGORY_VOICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_VOICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_VOICE => _id_CATEGORY_VOICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_VR_HOME = + _class.staticFieldId( + r'CATEGORY_VR_HOME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CATEGORY_VR_HOME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CATEGORY_VR_HOME => _id_CATEGORY_VR_HOME.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int CHOOSER_CONTENT_TYPE_ALBUM` + static const CHOOSER_CONTENT_TYPE_ALBUM = 1; + static final _id_CREATOR = + _class.staticFieldId( + r'CREATOR', + r'Landroid/os/Parcelable$Creator;', + ); + /// from: `static public final android.os.Parcelable$Creator CREATOR` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JObject? get CREATOR => _id_CREATOR.get(_class, const jni$_.$JObject$NullableType$()); + + static final _id_EXTRA_ALARM_COUNT = + _class.staticFieldId( + r'EXTRA_ALARM_COUNT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ALARM_COUNT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ALARM_COUNT => _id_EXTRA_ALARM_COUNT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ALLOW_MULTIPLE = + _class.staticFieldId( + r'EXTRA_ALLOW_MULTIPLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ALLOW_MULTIPLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ALLOW_MULTIPLE => _id_EXTRA_ALLOW_MULTIPLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ALLOW_REPLACE = + _class.staticFieldId( + r'EXTRA_ALLOW_REPLACE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ALLOW_REPLACE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ALLOW_REPLACE => _id_EXTRA_ALLOW_REPLACE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ALTERNATE_INTENTS = + _class.staticFieldId( + r'EXTRA_ALTERNATE_INTENTS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ALTERNATE_INTENTS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ALTERNATE_INTENTS => _id_EXTRA_ALTERNATE_INTENTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ARCHIVAL = + _class.staticFieldId( + r'EXTRA_ARCHIVAL', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ARCHIVAL` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ARCHIVAL => _id_EXTRA_ARCHIVAL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ASSIST_CONTEXT = + _class.staticFieldId( + r'EXTRA_ASSIST_CONTEXT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ASSIST_CONTEXT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ASSIST_CONTEXT => _id_EXTRA_ASSIST_CONTEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ASSIST_INPUT_DEVICE_ID = + _class.staticFieldId( + r'EXTRA_ASSIST_INPUT_DEVICE_ID', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ASSIST_INPUT_DEVICE_ID` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ASSIST_INPUT_DEVICE_ID => _id_EXTRA_ASSIST_INPUT_DEVICE_ID.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ASSIST_INPUT_HINT_KEYBOARD = + _class.staticFieldId( + r'EXTRA_ASSIST_INPUT_HINT_KEYBOARD', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ASSIST_INPUT_HINT_KEYBOARD` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ASSIST_INPUT_HINT_KEYBOARD => _id_EXTRA_ASSIST_INPUT_HINT_KEYBOARD.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ASSIST_PACKAGE = + _class.staticFieldId( + r'EXTRA_ASSIST_PACKAGE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ASSIST_PACKAGE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ASSIST_PACKAGE => _id_EXTRA_ASSIST_PACKAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ASSIST_UID = + _class.staticFieldId( + r'EXTRA_ASSIST_UID', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ASSIST_UID` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ASSIST_UID => _id_EXTRA_ASSIST_UID.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ATTRIBUTION_TAGS = + _class.staticFieldId( + r'EXTRA_ATTRIBUTION_TAGS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ATTRIBUTION_TAGS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ATTRIBUTION_TAGS => _id_EXTRA_ATTRIBUTION_TAGS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_AUTO_LAUNCH_SINGLE_CHOICE = + _class.staticFieldId( + r'EXTRA_AUTO_LAUNCH_SINGLE_CHOICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_AUTO_LAUNCH_SINGLE_CHOICE => _id_EXTRA_AUTO_LAUNCH_SINGLE_CHOICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_BCC = + _class.staticFieldId( + r'EXTRA_BCC', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_BCC` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_BCC => _id_EXTRA_BCC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_BUG_REPORT = + _class.staticFieldId( + r'EXTRA_BUG_REPORT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_BUG_REPORT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_BUG_REPORT => _id_EXTRA_BUG_REPORT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE = + _class.staticFieldId( + r'EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE => _id_EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CC = + _class.staticFieldId( + r'EXTRA_CC', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CC` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CC => _id_EXTRA_CC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHANGED_COMPONENT_NAME = + _class.staticFieldId( + r'EXTRA_CHANGED_COMPONENT_NAME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHANGED_COMPONENT_NAME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHANGED_COMPONENT_NAME => _id_EXTRA_CHANGED_COMPONENT_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHANGED_COMPONENT_NAME_LIST = + _class.staticFieldId( + r'EXTRA_CHANGED_COMPONENT_NAME_LIST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHANGED_COMPONENT_NAME_LIST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHANGED_COMPONENT_NAME_LIST => _id_EXTRA_CHANGED_COMPONENT_NAME_LIST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHANGED_PACKAGE_LIST = + _class.staticFieldId( + r'EXTRA_CHANGED_PACKAGE_LIST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHANGED_PACKAGE_LIST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHANGED_PACKAGE_LIST => _id_EXTRA_CHANGED_PACKAGE_LIST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHANGED_UID_LIST = + _class.staticFieldId( + r'EXTRA_CHANGED_UID_LIST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHANGED_UID_LIST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHANGED_UID_LIST => _id_EXTRA_CHANGED_UID_LIST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI = + _class.staticFieldId( + r'EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI => _id_EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_CONTENT_TYPE_HINT = + _class.staticFieldId( + r'EXTRA_CHOOSER_CONTENT_TYPE_HINT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOOSER_CONTENT_TYPE_HINT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOOSER_CONTENT_TYPE_HINT => _id_EXTRA_CHOOSER_CONTENT_TYPE_HINT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_CUSTOM_ACTIONS = + _class.staticFieldId( + r'EXTRA_CHOOSER_CUSTOM_ACTIONS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOOSER_CUSTOM_ACTIONS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOOSER_CUSTOM_ACTIONS => _id_EXTRA_CHOOSER_CUSTOM_ACTIONS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_FOCUSED_ITEM_POSITION = + _class.staticFieldId( + r'EXTRA_CHOOSER_FOCUSED_ITEM_POSITION', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOOSER_FOCUSED_ITEM_POSITION` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOOSER_FOCUSED_ITEM_POSITION => _id_EXTRA_CHOOSER_FOCUSED_ITEM_POSITION.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_MODIFY_SHARE_ACTION = + _class.staticFieldId( + r'EXTRA_CHOOSER_MODIFY_SHARE_ACTION', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOOSER_MODIFY_SHARE_ACTION` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOOSER_MODIFY_SHARE_ACTION => _id_EXTRA_CHOOSER_MODIFY_SHARE_ACTION.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = + _class.staticFieldId( + r'EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER => _id_EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_RESULT = + _class.staticFieldId( + r'EXTRA_CHOOSER_RESULT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOOSER_RESULT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOOSER_RESULT => _id_EXTRA_CHOOSER_RESULT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_RESULT_INTENT_SENDER = + _class.staticFieldId( + r'EXTRA_CHOOSER_RESULT_INTENT_SENDER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOOSER_RESULT_INTENT_SENDER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOOSER_RESULT_INTENT_SENDER => _id_EXTRA_CHOOSER_RESULT_INTENT_SENDER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_TARGETS = + _class.staticFieldId( + r'EXTRA_CHOOSER_TARGETS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOOSER_TARGETS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOOSER_TARGETS => _id_EXTRA_CHOOSER_TARGETS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOSEN_COMPONENT = + _class.staticFieldId( + r'EXTRA_CHOSEN_COMPONENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOSEN_COMPONENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOSEN_COMPONENT => _id_EXTRA_CHOSEN_COMPONENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = + _class.staticFieldId( + r'EXTRA_CHOSEN_COMPONENT_INTENT_SENDER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CHOSEN_COMPONENT_INTENT_SENDER => _id_EXTRA_CHOSEN_COMPONENT_INTENT_SENDER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_COMPONENT_NAME = + _class.staticFieldId( + r'EXTRA_COMPONENT_NAME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_COMPONENT_NAME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_COMPONENT_NAME => _id_EXTRA_COMPONENT_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CONTENT_ANNOTATIONS = + _class.staticFieldId( + r'EXTRA_CONTENT_ANNOTATIONS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CONTENT_ANNOTATIONS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CONTENT_ANNOTATIONS => _id_EXTRA_CONTENT_ANNOTATIONS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CONTENT_QUERY = + _class.staticFieldId( + r'EXTRA_CONTENT_QUERY', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_CONTENT_QUERY` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_CONTENT_QUERY => _id_EXTRA_CONTENT_QUERY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_DATA_REMOVED = + _class.staticFieldId( + r'EXTRA_DATA_REMOVED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_DATA_REMOVED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_DATA_REMOVED => _id_EXTRA_DATA_REMOVED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_DOCK_STATE = + _class.staticFieldId( + r'EXTRA_DOCK_STATE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_DOCK_STATE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_DOCK_STATE => _id_EXTRA_DOCK_STATE.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int EXTRA_DOCK_STATE_CAR` + static const EXTRA_DOCK_STATE_CAR = 2; + /// from: `static public final int EXTRA_DOCK_STATE_DESK` + static const EXTRA_DOCK_STATE_DESK = 1; + /// from: `static public final int EXTRA_DOCK_STATE_HE_DESK` + static const EXTRA_DOCK_STATE_HE_DESK = 4; + /// from: `static public final int EXTRA_DOCK_STATE_LE_DESK` + static const EXTRA_DOCK_STATE_LE_DESK = 3; + /// from: `static public final int EXTRA_DOCK_STATE_UNDOCKED` + static const EXTRA_DOCK_STATE_UNDOCKED = 0; + static final _id_EXTRA_DONT_KILL_APP = + _class.staticFieldId( + r'EXTRA_DONT_KILL_APP', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_DONT_KILL_APP` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_DONT_KILL_APP => _id_EXTRA_DONT_KILL_APP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_DURATION_MILLIS = + _class.staticFieldId( + r'EXTRA_DURATION_MILLIS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_DURATION_MILLIS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_DURATION_MILLIS => _id_EXTRA_DURATION_MILLIS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_EMAIL = + _class.staticFieldId( + r'EXTRA_EMAIL', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_EMAIL` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_EMAIL => _id_EXTRA_EMAIL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_END_TIME = + _class.staticFieldId( + r'EXTRA_END_TIME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_END_TIME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_END_TIME => _id_EXTRA_END_TIME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_EXCLUDE_COMPONENTS = + _class.staticFieldId( + r'EXTRA_EXCLUDE_COMPONENTS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_EXCLUDE_COMPONENTS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_EXCLUDE_COMPONENTS => _id_EXTRA_EXCLUDE_COMPONENTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_FROM_STORAGE = + _class.staticFieldId( + r'EXTRA_FROM_STORAGE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_FROM_STORAGE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_FROM_STORAGE => _id_EXTRA_FROM_STORAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_HTML_TEXT = + _class.staticFieldId( + r'EXTRA_HTML_TEXT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_HTML_TEXT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_HTML_TEXT => _id_EXTRA_HTML_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_INDEX = + _class.staticFieldId( + r'EXTRA_INDEX', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_INDEX` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_INDEX => _id_EXTRA_INDEX.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_INITIAL_INTENTS = + _class.staticFieldId( + r'EXTRA_INITIAL_INTENTS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_INITIAL_INTENTS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_INITIAL_INTENTS => _id_EXTRA_INITIAL_INTENTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_INSTALLER_PACKAGE_NAME = + _class.staticFieldId( + r'EXTRA_INSTALLER_PACKAGE_NAME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_INSTALLER_PACKAGE_NAME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_INSTALLER_PACKAGE_NAME => _id_EXTRA_INSTALLER_PACKAGE_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_INTENT = + _class.staticFieldId( + r'EXTRA_INTENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_INTENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_INTENT => _id_EXTRA_INTENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_KEY_EVENT = + _class.staticFieldId( + r'EXTRA_KEY_EVENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_KEY_EVENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_KEY_EVENT => _id_EXTRA_KEY_EVENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_LOCALE_LIST = + _class.staticFieldId( + r'EXTRA_LOCALE_LIST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_LOCALE_LIST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_LOCALE_LIST => _id_EXTRA_LOCALE_LIST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_LOCAL_ONLY = + _class.staticFieldId( + r'EXTRA_LOCAL_ONLY', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_LOCAL_ONLY` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_LOCAL_ONLY => _id_EXTRA_LOCAL_ONLY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_LOCUS_ID = + _class.staticFieldId( + r'EXTRA_LOCUS_ID', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_LOCUS_ID` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_LOCUS_ID => _id_EXTRA_LOCUS_ID.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_METADATA_TEXT = + _class.staticFieldId( + r'EXTRA_METADATA_TEXT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_METADATA_TEXT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_METADATA_TEXT => _id_EXTRA_METADATA_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_MIME_TYPES = + _class.staticFieldId( + r'EXTRA_MIME_TYPES', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_MIME_TYPES` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_MIME_TYPES => _id_EXTRA_MIME_TYPES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_NOT_UNKNOWN_SOURCE = + _class.staticFieldId( + r'EXTRA_NOT_UNKNOWN_SOURCE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_NOT_UNKNOWN_SOURCE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_NOT_UNKNOWN_SOURCE => _id_EXTRA_NOT_UNKNOWN_SOURCE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ORIGINATING_URI = + _class.staticFieldId( + r'EXTRA_ORIGINATING_URI', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_ORIGINATING_URI` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_ORIGINATING_URI => _id_EXTRA_ORIGINATING_URI.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PACKAGES = + _class.staticFieldId( + r'EXTRA_PACKAGES', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_PACKAGES` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_PACKAGES => _id_EXTRA_PACKAGES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PACKAGE_NAME = + _class.staticFieldId( + r'EXTRA_PACKAGE_NAME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_PACKAGE_NAME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_PACKAGE_NAME => _id_EXTRA_PACKAGE_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PERMISSION_GROUP_NAME = + _class.staticFieldId( + r'EXTRA_PERMISSION_GROUP_NAME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_PERMISSION_GROUP_NAME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_PERMISSION_GROUP_NAME => _id_EXTRA_PERMISSION_GROUP_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PHONE_NUMBER = + _class.staticFieldId( + r'EXTRA_PHONE_NUMBER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_PHONE_NUMBER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_PHONE_NUMBER => _id_EXTRA_PHONE_NUMBER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PROCESS_TEXT = + _class.staticFieldId( + r'EXTRA_PROCESS_TEXT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_PROCESS_TEXT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_PROCESS_TEXT => _id_EXTRA_PROCESS_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PROCESS_TEXT_READONLY = + _class.staticFieldId( + r'EXTRA_PROCESS_TEXT_READONLY', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_PROCESS_TEXT_READONLY` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_PROCESS_TEXT_READONLY => _id_EXTRA_PROCESS_TEXT_READONLY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_QUICK_VIEW_FEATURES = + _class.staticFieldId( + r'EXTRA_QUICK_VIEW_FEATURES', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_QUICK_VIEW_FEATURES` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_QUICK_VIEW_FEATURES => _id_EXTRA_QUICK_VIEW_FEATURES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_QUIET_MODE = + _class.staticFieldId( + r'EXTRA_QUIET_MODE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_QUIET_MODE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_QUIET_MODE => _id_EXTRA_QUIET_MODE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_REFERRER = + _class.staticFieldId( + r'EXTRA_REFERRER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_REFERRER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_REFERRER => _id_EXTRA_REFERRER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_REFERRER_NAME = + _class.staticFieldId( + r'EXTRA_REFERRER_NAME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_REFERRER_NAME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_REFERRER_NAME => _id_EXTRA_REFERRER_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_REMOTE_INTENT_TOKEN = + _class.staticFieldId( + r'EXTRA_REMOTE_INTENT_TOKEN', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_REMOTE_INTENT_TOKEN` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_REMOTE_INTENT_TOKEN => _id_EXTRA_REMOTE_INTENT_TOKEN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_REPLACEMENT_EXTRAS = + _class.staticFieldId( + r'EXTRA_REPLACEMENT_EXTRAS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_REPLACEMENT_EXTRAS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_REPLACEMENT_EXTRAS => _id_EXTRA_REPLACEMENT_EXTRAS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_REPLACING = + _class.staticFieldId( + r'EXTRA_REPLACING', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_REPLACING` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_REPLACING => _id_EXTRA_REPLACING.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_RESTRICTIONS_BUNDLE = + _class.staticFieldId( + r'EXTRA_RESTRICTIONS_BUNDLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_RESTRICTIONS_BUNDLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_RESTRICTIONS_BUNDLE => _id_EXTRA_RESTRICTIONS_BUNDLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_RESTRICTIONS_INTENT = + _class.staticFieldId( + r'EXTRA_RESTRICTIONS_INTENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_RESTRICTIONS_INTENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_RESTRICTIONS_INTENT => _id_EXTRA_RESTRICTIONS_INTENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_RESTRICTIONS_LIST = + _class.staticFieldId( + r'EXTRA_RESTRICTIONS_LIST', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_RESTRICTIONS_LIST` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_RESTRICTIONS_LIST => _id_EXTRA_RESTRICTIONS_LIST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_RESULT_RECEIVER = + _class.staticFieldId( + r'EXTRA_RESULT_RECEIVER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_RESULT_RECEIVER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_RESULT_RECEIVER => _id_EXTRA_RESULT_RECEIVER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_RETURN_RESULT = + _class.staticFieldId( + r'EXTRA_RETURN_RESULT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_RETURN_RESULT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_RETURN_RESULT => _id_EXTRA_RETURN_RESULT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHORTCUT_ICON = + _class.staticFieldId( + r'EXTRA_SHORTCUT_ICON', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_SHORTCUT_ICON` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_SHORTCUT_ICON => _id_EXTRA_SHORTCUT_ICON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHORTCUT_ICON_RESOURCE = + _class.staticFieldId( + r'EXTRA_SHORTCUT_ICON_RESOURCE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_SHORTCUT_ICON_RESOURCE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_SHORTCUT_ICON_RESOURCE => _id_EXTRA_SHORTCUT_ICON_RESOURCE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHORTCUT_ID = + _class.staticFieldId( + r'EXTRA_SHORTCUT_ID', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_SHORTCUT_ID` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_SHORTCUT_ID => _id_EXTRA_SHORTCUT_ID.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHORTCUT_INTENT = + _class.staticFieldId( + r'EXTRA_SHORTCUT_INTENT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_SHORTCUT_INTENT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_SHORTCUT_INTENT => _id_EXTRA_SHORTCUT_INTENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHORTCUT_NAME = + _class.staticFieldId( + r'EXTRA_SHORTCUT_NAME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_SHORTCUT_NAME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_SHORTCUT_NAME => _id_EXTRA_SHORTCUT_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHUTDOWN_USERSPACE_ONLY = + _class.staticFieldId( + r'EXTRA_SHUTDOWN_USERSPACE_ONLY', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_SHUTDOWN_USERSPACE_ONLY` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_SHUTDOWN_USERSPACE_ONLY => _id_EXTRA_SHUTDOWN_USERSPACE_ONLY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SPLIT_NAME = + _class.staticFieldId( + r'EXTRA_SPLIT_NAME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_SPLIT_NAME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_SPLIT_NAME => _id_EXTRA_SPLIT_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_START_TIME = + _class.staticFieldId( + r'EXTRA_START_TIME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_START_TIME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_START_TIME => _id_EXTRA_START_TIME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_STREAM = + _class.staticFieldId( + r'EXTRA_STREAM', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_STREAM` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_STREAM => _id_EXTRA_STREAM.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SUBJECT = + _class.staticFieldId( + r'EXTRA_SUBJECT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_SUBJECT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_SUBJECT => _id_EXTRA_SUBJECT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SUSPENDED_PACKAGE_EXTRAS = + _class.staticFieldId( + r'EXTRA_SUSPENDED_PACKAGE_EXTRAS', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_SUSPENDED_PACKAGE_EXTRAS` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_SUSPENDED_PACKAGE_EXTRAS => _id_EXTRA_SUSPENDED_PACKAGE_EXTRAS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_TEMPLATE = + _class.staticFieldId( + r'EXTRA_TEMPLATE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_TEMPLATE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_TEMPLATE => _id_EXTRA_TEMPLATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_TEXT = + _class.staticFieldId( + r'EXTRA_TEXT', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_TEXT` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_TEXT => _id_EXTRA_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_TIME = + _class.staticFieldId( + r'EXTRA_TIME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_TIME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_TIME => _id_EXTRA_TIME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_TIMEZONE = + _class.staticFieldId( + r'EXTRA_TIMEZONE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_TIMEZONE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_TIMEZONE => _id_EXTRA_TIMEZONE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_TITLE = + _class.staticFieldId( + r'EXTRA_TITLE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_TITLE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_TITLE => _id_EXTRA_TITLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_UID = + _class.staticFieldId( + r'EXTRA_UID', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_UID` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_UID => _id_EXTRA_UID.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_USER = + _class.staticFieldId( + r'EXTRA_USER', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_USER` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_USER => _id_EXTRA_USER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_USER_INITIATED = + _class.staticFieldId( + r'EXTRA_USER_INITIATED', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_USER_INITIATED` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_USER_INITIATED => _id_EXTRA_USER_INITIATED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_USE_STYLUS_MODE = + _class.staticFieldId( + r'EXTRA_USE_STYLUS_MODE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EXTRA_USE_STYLUS_MODE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EXTRA_USE_STYLUS_MODE => _id_EXTRA_USE_STYLUS_MODE.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int FILL_IN_ACTION` + static const FILL_IN_ACTION = 1; + /// from: `static public final int FILL_IN_CATEGORIES` + static const FILL_IN_CATEGORIES = 4; + /// from: `static public final int FILL_IN_CLIP_DATA` + static const FILL_IN_CLIP_DATA = 128; + /// from: `static public final int FILL_IN_COMPONENT` + static const FILL_IN_COMPONENT = 8; + /// from: `static public final int FILL_IN_DATA` + static const FILL_IN_DATA = 2; + /// from: `static public final int FILL_IN_IDENTIFIER` + static const FILL_IN_IDENTIFIER = 256; + /// from: `static public final int FILL_IN_PACKAGE` + static const FILL_IN_PACKAGE = 16; + /// from: `static public final int FILL_IN_SELECTOR` + static const FILL_IN_SELECTOR = 64; + /// from: `static public final int FILL_IN_SOURCE_BOUNDS` + static const FILL_IN_SOURCE_BOUNDS = 32; + /// from: `static public final int FLAG_ACTIVITY_BROUGHT_TO_FRONT` + static const FLAG_ACTIVITY_BROUGHT_TO_FRONT = 4194304; + /// from: `static public final int FLAG_ACTIVITY_CLEAR_TASK` + static const FLAG_ACTIVITY_CLEAR_TASK = 32768; + /// from: `static public final int FLAG_ACTIVITY_CLEAR_TOP` + static const FLAG_ACTIVITY_CLEAR_TOP = 67108864; + /// from: `static public final int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET` + static const FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 524288; + /// from: `static public final int FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS` + static const FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS = 8388608; + /// from: `static public final int FLAG_ACTIVITY_FORWARD_RESULT` + static const FLAG_ACTIVITY_FORWARD_RESULT = 33554432; + /// from: `static public final int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY` + static const FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 1048576; + /// from: `static public final int FLAG_ACTIVITY_LAUNCH_ADJACENT` + static const FLAG_ACTIVITY_LAUNCH_ADJACENT = 4096; + /// from: `static public final int FLAG_ACTIVITY_MATCH_EXTERNAL` + static const FLAG_ACTIVITY_MATCH_EXTERNAL = 2048; + /// from: `static public final int FLAG_ACTIVITY_MULTIPLE_TASK` + static const FLAG_ACTIVITY_MULTIPLE_TASK = 134217728; + /// from: `static public final int FLAG_ACTIVITY_NEW_DOCUMENT` + static const FLAG_ACTIVITY_NEW_DOCUMENT = 524288; + /// from: `static public final int FLAG_ACTIVITY_NEW_TASK` + static const FLAG_ACTIVITY_NEW_TASK = 268435456; + /// from: `static public final int FLAG_ACTIVITY_NO_ANIMATION` + static const FLAG_ACTIVITY_NO_ANIMATION = 65536; + /// from: `static public final int FLAG_ACTIVITY_NO_HISTORY` + static const FLAG_ACTIVITY_NO_HISTORY = 1073741824; + /// from: `static public final int FLAG_ACTIVITY_NO_USER_ACTION` + static const FLAG_ACTIVITY_NO_USER_ACTION = 262144; + /// from: `static public final int FLAG_ACTIVITY_PREVIOUS_IS_TOP` + static const FLAG_ACTIVITY_PREVIOUS_IS_TOP = 16777216; + /// from: `static public final int FLAG_ACTIVITY_REORDER_TO_FRONT` + static const FLAG_ACTIVITY_REORDER_TO_FRONT = 131072; + /// from: `static public final int FLAG_ACTIVITY_REQUIRE_DEFAULT` + static const FLAG_ACTIVITY_REQUIRE_DEFAULT = 512; + /// from: `static public final int FLAG_ACTIVITY_REQUIRE_NON_BROWSER` + static const FLAG_ACTIVITY_REQUIRE_NON_BROWSER = 1024; + /// from: `static public final int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED` + static const FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 2097152; + /// from: `static public final int FLAG_ACTIVITY_RETAIN_IN_RECENTS` + static const FLAG_ACTIVITY_RETAIN_IN_RECENTS = 8192; + /// from: `static public final int FLAG_ACTIVITY_SINGLE_TOP` + static const FLAG_ACTIVITY_SINGLE_TOP = 536870912; + /// from: `static public final int FLAG_ACTIVITY_TASK_ON_HOME` + static const FLAG_ACTIVITY_TASK_ON_HOME = 16384; + /// from: `static public final int FLAG_DEBUG_LOG_RESOLUTION` + static const FLAG_DEBUG_LOG_RESOLUTION = 8; + /// from: `static public final int FLAG_DIRECT_BOOT_AUTO` + static const FLAG_DIRECT_BOOT_AUTO = 256; + /// from: `static public final int FLAG_EXCLUDE_STOPPED_PACKAGES` + static const FLAG_EXCLUDE_STOPPED_PACKAGES = 16; + /// from: `static public final int FLAG_FROM_BACKGROUND` + static const FLAG_FROM_BACKGROUND = 4; + /// from: `static public final int FLAG_GRANT_PERSISTABLE_URI_PERMISSION` + static const FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 64; + /// from: `static public final int FLAG_GRANT_PREFIX_URI_PERMISSION` + static const FLAG_GRANT_PREFIX_URI_PERMISSION = 128; + /// from: `static public final int FLAG_GRANT_READ_URI_PERMISSION` + static const FLAG_GRANT_READ_URI_PERMISSION = 1; + /// from: `static public final int FLAG_GRANT_WRITE_URI_PERMISSION` + static const FLAG_GRANT_WRITE_URI_PERMISSION = 2; + /// from: `static public final int FLAG_INCLUDE_STOPPED_PACKAGES` + static const FLAG_INCLUDE_STOPPED_PACKAGES = 32; + /// from: `static public final int FLAG_RECEIVER_FOREGROUND` + static const FLAG_RECEIVER_FOREGROUND = 268435456; + /// from: `static public final int FLAG_RECEIVER_NO_ABORT` + static const FLAG_RECEIVER_NO_ABORT = 134217728; + /// from: `static public final int FLAG_RECEIVER_REGISTERED_ONLY` + static const FLAG_RECEIVER_REGISTERED_ONLY = 1073741824; + /// from: `static public final int FLAG_RECEIVER_REPLACE_PENDING` + static const FLAG_RECEIVER_REPLACE_PENDING = 536870912; + /// from: `static public final int FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS` + static const FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS = 2097152; + static final _id_METADATA_DOCK_HOME = + _class.staticFieldId( + r'METADATA_DOCK_HOME', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String METADATA_DOCK_HOME` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get METADATA_DOCK_HOME => _id_METADATA_DOCK_HOME.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int URI_ALLOW_UNSAFE` + static const URI_ALLOW_UNSAFE = 4; + /// from: `static public final int URI_ANDROID_APP_SCHEME` + static const URI_ANDROID_APP_SCHEME = 2; + /// from: `static public final int URI_INTENT_SCHEME` + static const URI_INTENT_SCHEME = 1; + static final _id_new$ = _class.constructorId( + r'()V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory Intent() { + + + return Intent.fromReference( + + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference + ); + } + + static final _id_new$1 = _class.constructorId( + r'(Landroid/content/Context;Ljava/lang/Class;)V', + ); + + static final _new$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void (android.content.Context context, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$1(Context? context, jni$_.JObject? class$, ) { + + final _$context = context?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + + _new$1(_class.reference.pointer, _id_new$1 as jni$_.JMethodIDPtr, _$context.pointer, _$class$.pointer).reference + ); + } + + static final _id_new$2 = _class.constructorId( + r'(Landroid/content/Intent;)V', + ); + + static final _new$2 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void (android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$2(Intent? intent, ) { + + final _$intent = intent?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + + _new$2(_class.reference.pointer, _id_new$2 as jni$_.JMethodIDPtr, _$intent.pointer).reference + ); + } + + static final _id_new$3 = _class.constructorId( + r'(Ljava/lang/String;)V', + ); + + static final _new$3 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void (java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$3(jni$_.JString? string, ) { + + final _$string = string?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + + _new$3(_class.reference.pointer, _id_new$3 as jni$_.JMethodIDPtr, _$string.pointer).reference + ); + } + + static final _id_new$4 = _class.constructorId( + r'(Ljava/lang/String;Landroid/net/Uri;)V', + ); + + static final _new$4 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void (java.lang.String string, android.net.Uri uri)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$4(jni$_.JString? string, jni$_.JObject? uri, ) { + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$uri = uri?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + + _new$4(_class.reference.pointer, _id_new$4 as jni$_.JMethodIDPtr, _$string.pointer, _$uri.pointer).reference + ); + } + + static final _id_new$5 = _class.constructorId( + r'(Ljava/lang/String;Landroid/net/Uri;Landroid/content/Context;Ljava/lang/Class;)V', + ); + + static final _new$5 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void (java.lang.String string, android.net.Uri uri, android.content.Context context, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$5(jni$_.JString? string, jni$_.JObject? uri, Context? context, jni$_.JObject? class$, ) { + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$context = context?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + + _new$5(_class.reference.pointer, _id_new$5 as jni$_.JMethodIDPtr, _$string.pointer, _$uri.pointer, _$context.pointer, _$class$.pointer).reference + ); + } + + static final _id_addCategory = _class.instanceMethodId( + r'addCategory', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _addCategory = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent addCategory(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? addCategory(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _addCategory(reference.pointer, _id_addCategory as jni$_.JMethodIDPtr, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_addFlags = _class.instanceMethodId( + r'addFlags', + r'(I)Landroid/content/Intent;', + ); + + static final _addFlags = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public android.content.Intent addFlags(int i)` + /// The returned object must be released after use, by calling the [release] method. + Intent? addFlags(int i, ){ + + + return _addFlags(reference.pointer, _id_addFlags as jni$_.JMethodIDPtr, i).object(const $Intent$NullableType$()); + } + + static final _id_clone = _class.instanceMethodId( + r'clone', + r'()Ljava/lang/Object;', + ); + + static final _clone = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.Object clone()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? clone(){ + + + return _clone(reference.pointer, _id_clone as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_cloneFilter = _class.instanceMethodId( + r'cloneFilter', + r'()Landroid/content/Intent;', + ); + + static final _cloneFilter = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.content.Intent cloneFilter()` + /// The returned object must be released after use, by calling the [release] method. + Intent? cloneFilter(){ + + + return _cloneFilter(reference.pointer, _id_cloneFilter as jni$_.JMethodIDPtr).object(const $Intent$NullableType$()); + } + + static final _id_createChooser = _class.staticMethodId( + r'createChooser', + r'(Landroid/content/Intent;Ljava/lang/CharSequence;)Landroid/content/Intent;', + ); + + static final _createChooser = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `static public android.content.Intent createChooser(android.content.Intent intent, java.lang.CharSequence charSequence)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? createChooser(Intent? intent, jni$_.JObject? charSequence, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + return _createChooser(_class.reference.pointer, _id_createChooser as jni$_.JMethodIDPtr, _$intent.pointer, _$charSequence.pointer).object(const $Intent$NullableType$()); + } + + static final _id_createChooser$1 = _class.staticMethodId( + r'createChooser', + r'(Landroid/content/Intent;Ljava/lang/CharSequence;Landroid/content/IntentSender;)Landroid/content/Intent;', + ); + + static final _createChooser$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `static public android.content.Intent createChooser(android.content.Intent intent, java.lang.CharSequence charSequence, android.content.IntentSender intentSender)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? createChooser$1(Intent? intent, jni$_.JObject? charSequence, jni$_.JObject? intentSender, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + final _$intentSender = intentSender?.reference ?? jni$_.jNullReference; + return _createChooser$1(_class.reference.pointer, _id_createChooser$1 as jni$_.JMethodIDPtr, _$intent.pointer, _$charSequence.pointer, _$intentSender.pointer).object(const $Intent$NullableType$()); + } + + static final _id_describeContents = _class.instanceMethodId( + r'describeContents', + r'()I', + ); + + static final _describeContents = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int describeContents()` + int describeContents(){ + + + return _describeContents(reference.pointer, _id_describeContents as jni$_.JMethodIDPtr).integer; + } + + static final _id_fillIn = _class.instanceMethodId( + r'fillIn', + r'(Landroid/content/Intent;I)I', + ); + + static final _fillIn = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public int fillIn(android.content.Intent intent, int i)` + int fillIn(Intent? intent, int i, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _fillIn(reference.pointer, _id_fillIn as jni$_.JMethodIDPtr, _$intent.pointer, i).integer; + } + + static final _id_filterEquals = _class.instanceMethodId( + r'filterEquals', + r'(Landroid/content/Intent;)Z', + ); + + static final _filterEquals = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public boolean filterEquals(android.content.Intent intent)` + bool filterEquals(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _filterEquals(reference.pointer, _id_filterEquals as jni$_.JMethodIDPtr, _$intent.pointer).boolean; + } + + static final _id_filterHashCode = _class.instanceMethodId( + r'filterHashCode', + r'()I', + ); + + static final _filterHashCode = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int filterHashCode()` + int filterHashCode(){ + + + return _filterHashCode(reference.pointer, _id_filterHashCode as jni$_.JMethodIDPtr).integer; + } + + static final _id_getAction = _class.instanceMethodId( + r'getAction', + r'()Ljava/lang/String;', + ); + + static final _getAction = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String getAction()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getAction(){ + + + return _getAction(reference.pointer, _id_getAction as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getBooleanArrayExtra = _class.instanceMethodId( + r'getBooleanArrayExtra', + r'(Ljava/lang/String;)[Z', + ); + + static final _getBooleanArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public boolean[] getBooleanArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JBooleanArray? getBooleanArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getBooleanArrayExtra(reference.pointer, _id_getBooleanArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JBooleanArray$NullableType$()); + } + + static final _id_getBooleanExtra = _class.instanceMethodId( + r'getBooleanExtra', + r'(Ljava/lang/String;Z)Z', + ); + + static final _getBooleanExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public boolean getBooleanExtra(java.lang.String string, boolean z)` + bool getBooleanExtra(jni$_.JString? string, bool z, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getBooleanExtra(reference.pointer, _id_getBooleanExtra as jni$_.JMethodIDPtr, _$string.pointer, z ? 1 : 0).boolean; + } + + static final _id_getBundleExtra = _class.instanceMethodId( + r'getBundleExtra', + r'(Ljava/lang/String;)Landroid/os/Bundle;', + ); + + static final _getBundleExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.os.Bundle getBundleExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Bundle? getBundleExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getBundleExtra(reference.pointer, _id_getBundleExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const $Bundle$NullableType$()); + } + + static final _id_getByteArrayExtra = _class.instanceMethodId( + r'getByteArrayExtra', + r'(Ljava/lang/String;)[B', + ); + + static final _getByteArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public byte[] getByteArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JByteArray? getByteArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getByteArrayExtra(reference.pointer, _id_getByteArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JByteArray$NullableType$()); + } + + static final _id_getByteExtra = _class.instanceMethodId( + r'getByteExtra', + r'(Ljava/lang/String;B)B', + ); + + static final _getByteExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallByteMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public byte getByteExtra(java.lang.String string, byte b)` + int getByteExtra(jni$_.JString? string, int b, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getByteExtra(reference.pointer, _id_getByteExtra as jni$_.JMethodIDPtr, _$string.pointer, b).byte; + } + + static final _id_getCategories = _class.instanceMethodId( + r'getCategories', + r'()Ljava/util/Set;', + ); + + static final _getCategories = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.util.Set getCategories()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JSet? getCategories(){ + + + return _getCategories(reference.pointer, _id_getCategories as jni$_.JMethodIDPtr).object?>(const jni$_.$JSet$NullableType$(jni$_.$JString$NullableType$())); + } + + static final _id_getCharArrayExtra = _class.instanceMethodId( + r'getCharArrayExtra', + r'(Ljava/lang/String;)[C', + ); + + static final _getCharArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public char[] getCharArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JCharArray? getCharArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharArrayExtra(reference.pointer, _id_getCharArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JCharArray$NullableType$()); + } + + static final _id_getCharExtra = _class.instanceMethodId( + r'getCharExtra', + r'(Ljava/lang/String;C)C', + ); + + static final _getCharExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallCharMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public char getCharExtra(java.lang.String string, char c)` + int getCharExtra(jni$_.JString? string, int c, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharExtra(reference.pointer, _id_getCharExtra as jni$_.JMethodIDPtr, _$string.pointer, c).char; + } + + static final _id_getCharSequenceArrayExtra = _class.instanceMethodId( + r'getCharSequenceArrayExtra', + r'(Ljava/lang/String;)[Ljava/lang/CharSequence;', + ); + + static final _getCharSequenceArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.lang.CharSequence[] getCharSequenceArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getCharSequenceArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharSequenceArrayExtra(reference.pointer, _id_getCharSequenceArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object?>(const jni$_.$JArray$NullableType$(jni$_.$JObject$NullableType$())); + } + + static final _id_getCharSequenceArrayListExtra = _class.instanceMethodId( + r'getCharSequenceArrayListExtra', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getCharSequenceArrayListExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getCharSequenceArrayListExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCharSequenceArrayListExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharSequenceArrayListExtra(reference.pointer, _id_getCharSequenceArrayListExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getCharSequenceExtra = _class.instanceMethodId( + r'getCharSequenceExtra', + r'(Ljava/lang/String;)Ljava/lang/CharSequence;', + ); + + static final _getCharSequenceExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.lang.CharSequence getCharSequenceExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCharSequenceExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharSequenceExtra(reference.pointer, _id_getCharSequenceExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getClipData = _class.instanceMethodId( + r'getClipData', + r'()Landroid/content/ClipData;', + ); + + static final _getClipData = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.content.ClipData getClipData()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getClipData(){ + + + return _getClipData(reference.pointer, _id_getClipData as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getComponent = _class.instanceMethodId( + r'getComponent', + r'()Landroid/content/ComponentName;', + ); + + static final _getComponent = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.content.ComponentName getComponent()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getComponent(){ + + + return _getComponent(reference.pointer, _id_getComponent as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getData = _class.instanceMethodId( + r'getData', + r'()Landroid/net/Uri;', + ); + + static final _getData = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.net.Uri getData()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getData(){ + + + return _getData(reference.pointer, _id_getData as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getDataString = _class.instanceMethodId( + r'getDataString', + r'()Ljava/lang/String;', + ); + + static final _getDataString = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String getDataString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getDataString(){ + + + return _getDataString(reference.pointer, _id_getDataString as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getDoubleArrayExtra = _class.instanceMethodId( + r'getDoubleArrayExtra', + r'(Ljava/lang/String;)[D', + ); + + static final _getDoubleArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public double[] getDoubleArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JDoubleArray? getDoubleArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getDoubleArrayExtra(reference.pointer, _id_getDoubleArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JDoubleArray$NullableType$()); + } + + static final _id_getDoubleExtra = _class.instanceMethodId( + r'getDoubleExtra', + r'(Ljava/lang/String;D)D', + ); + + static final _getDoubleExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>)>>('globalEnv_CallDoubleMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, double)>(); + /// from: `public double getDoubleExtra(java.lang.String string, double d)` + double getDoubleExtra(jni$_.JString? string, double d, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getDoubleExtra(reference.pointer, _id_getDoubleExtra as jni$_.JMethodIDPtr, _$string.pointer, d).doubleFloat; + } + + static final _id_getExtras = _class.instanceMethodId( + r'getExtras', + r'()Landroid/os/Bundle;', + ); + + static final _getExtras = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.os.Bundle getExtras()` + /// The returned object must be released after use, by calling the [release] method. + Bundle? getExtras(){ + + + return _getExtras(reference.pointer, _id_getExtras as jni$_.JMethodIDPtr).object(const $Bundle$NullableType$()); + } + + static final _id_getFlags = _class.instanceMethodId( + r'getFlags', + r'()I', + ); + + static final _getFlags = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int getFlags()` + int getFlags(){ + + + return _getFlags(reference.pointer, _id_getFlags as jni$_.JMethodIDPtr).integer; + } + + static final _id_getFloatArrayExtra = _class.instanceMethodId( + r'getFloatArrayExtra', + r'(Ljava/lang/String;)[F', + ); + + static final _getFloatArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public float[] getFloatArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JFloatArray? getFloatArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getFloatArrayExtra(reference.pointer, _id_getFloatArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JFloatArray$NullableType$()); + } + + static final _id_getFloatExtra = _class.instanceMethodId( + r'getFloatExtra', + r'(Ljava/lang/String;F)F', + ); + + static final _getFloatExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>)>>('globalEnv_CallFloatMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, double)>(); + /// from: `public float getFloatExtra(java.lang.String string, float f)` + double getFloatExtra(jni$_.JString? string, double f, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getFloatExtra(reference.pointer, _id_getFloatExtra as jni$_.JMethodIDPtr, _$string.pointer, f).float; + } + + static final _id_getIdentifier = _class.instanceMethodId( + r'getIdentifier', + r'()Ljava/lang/String;', + ); + + static final _getIdentifier = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String getIdentifier()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getIdentifier(){ + + + return _getIdentifier(reference.pointer, _id_getIdentifier as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getIntArrayExtra = _class.instanceMethodId( + r'getIntArrayExtra', + r'(Ljava/lang/String;)[I', + ); + + static final _getIntArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public int[] getIntArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JIntArray? getIntArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntArrayExtra(reference.pointer, _id_getIntArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JIntArray$NullableType$()); + } + + static final _id_getIntExtra = _class.instanceMethodId( + r'getIntExtra', + r'(Ljava/lang/String;I)I', + ); + + static final _getIntExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public int getIntExtra(java.lang.String string, int i)` + int getIntExtra(jni$_.JString? string, int i, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntExtra(reference.pointer, _id_getIntExtra as jni$_.JMethodIDPtr, _$string.pointer, i).integer; + } + + static final _id_getIntegerArrayListExtra = _class.instanceMethodId( + r'getIntegerArrayListExtra', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getIntegerArrayListExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getIntegerArrayListExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getIntegerArrayListExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntegerArrayListExtra(reference.pointer, _id_getIntegerArrayListExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getIntent = _class.staticMethodId( + r'getIntent', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _getIntent = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `static public android.content.Intent getIntent(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? getIntent(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntent(_class.reference.pointer, _id_getIntent as jni$_.JMethodIDPtr, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_getIntentOld = _class.staticMethodId( + r'getIntentOld', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _getIntentOld = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `static public android.content.Intent getIntentOld(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? getIntentOld(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntentOld(_class.reference.pointer, _id_getIntentOld as jni$_.JMethodIDPtr, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_getLongArrayExtra = _class.instanceMethodId( + r'getLongArrayExtra', + r'(Ljava/lang/String;)[J', + ); + + static final _getLongArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public long[] getLongArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JLongArray? getLongArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getLongArrayExtra(reference.pointer, _id_getLongArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JLongArray$NullableType$()); + } + + static final _id_getLongExtra = _class.instanceMethodId( + r'getLongExtra', + r'(Ljava/lang/String;J)J', + ); + + static final _getLongExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int64)>)>>('globalEnv_CallLongMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public long getLongExtra(java.lang.String string, long j)` + int getLongExtra(jni$_.JString? string, int j, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getLongExtra(reference.pointer, _id_getLongExtra as jni$_.JMethodIDPtr, _$string.pointer, j).long; + } + + static final _id_getPackage = _class.instanceMethodId( + r'getPackage', + r'()Ljava/lang/String;', + ); + + static final _getPackage = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String getPackage()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getPackage(){ + + + return _getPackage(reference.pointer, _id_getPackage as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getParcelableArrayExtra = _class.instanceMethodId( + r'getParcelableArrayExtra', + r'(Ljava/lang/String;)[Landroid/os/Parcelable;', + ); + + static final _getParcelableArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.os.Parcelable[] getParcelableArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getParcelableArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getParcelableArrayExtra(reference.pointer, _id_getParcelableArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object?>(const jni$_.$JArray$NullableType$(jni$_.$JObject$NullableType$())); + } + + static final _id_getParcelableArrayExtra$1 = _class.instanceMethodId( + r'getParcelableArrayExtra', + r'(Ljava/lang/String;Ljava/lang/Class;)[Ljava/lang/Object;', + ); + + static final _getParcelableArrayExtra$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public T[] getParcelableArrayExtra(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray<$T?>? getParcelableArrayExtra$1<$T extends jni$_.JObject?>(jni$_.JString? string, jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getParcelableArrayExtra$1(reference.pointer, _id_getParcelableArrayExtra$1 as jni$_.JMethodIDPtr, _$string.pointer, _$class$.pointer).object?>(jni$_.$JArray$NullableType$<$T?>(T.nullableType)); + } + + static final _id_getParcelableArrayListExtra = _class.instanceMethodId( + r'getParcelableArrayListExtra', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getParcelableArrayListExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getParcelableArrayListExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getParcelableArrayListExtra<$T extends jni$_.JObject?>(jni$_.JString? string, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getParcelableArrayListExtra(reference.pointer, _id_getParcelableArrayListExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getParcelableArrayListExtra$1 = _class.instanceMethodId( + r'getParcelableArrayListExtra', + r'(Ljava/lang/String;Ljava/lang/Class;)Ljava/util/ArrayList;', + ); + + static final _getParcelableArrayListExtra$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getParcelableArrayListExtra(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getParcelableArrayListExtra$1<$T extends jni$_.JObject?>(jni$_.JString? string, jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getParcelableArrayListExtra$1(reference.pointer, _id_getParcelableArrayListExtra$1 as jni$_.JMethodIDPtr, _$string.pointer, _$class$.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getParcelableExtra = _class.instanceMethodId( + r'getParcelableExtra', + r'(Ljava/lang/String;)Landroid/os/Parcelable;', + ); + + static final _getParcelableExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public T getParcelableExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + $T? getParcelableExtra<$T extends jni$_.JObject?>(jni$_.JString? string, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getParcelableExtra(reference.pointer, _id_getParcelableExtra as jni$_.JMethodIDPtr, _$string.pointer).object<$T?>(T.nullableType); + } + + static final _id_getParcelableExtra$1 = _class.instanceMethodId( + r'getParcelableExtra', + r'(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;', + ); + + static final _getParcelableExtra$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public T getParcelableExtra(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + $T? getParcelableExtra$1<$T extends jni$_.JObject?>(jni$_.JString? string, jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getParcelableExtra$1(reference.pointer, _id_getParcelableExtra$1 as jni$_.JMethodIDPtr, _$string.pointer, _$class$.pointer).object<$T?>(T.nullableType); + } + + static final _id_getScheme = _class.instanceMethodId( + r'getScheme', + r'()Ljava/lang/String;', + ); + + static final _getScheme = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String getScheme()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getScheme(){ + + + return _getScheme(reference.pointer, _id_getScheme as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getSelector = _class.instanceMethodId( + r'getSelector', + r'()Landroid/content/Intent;', + ); + + static final _getSelector = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.content.Intent getSelector()` + /// The returned object must be released after use, by calling the [release] method. + Intent? getSelector(){ + + + return _getSelector(reference.pointer, _id_getSelector as jni$_.JMethodIDPtr).object(const $Intent$NullableType$()); + } + + static final _id_getSerializableExtra = _class.instanceMethodId( + r'getSerializableExtra', + r'(Ljava/lang/String;)Ljava/io/Serializable;', + ); + + static final _getSerializableExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.io.Serializable getSerializableExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSerializableExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getSerializableExtra(reference.pointer, _id_getSerializableExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSerializableExtra$1 = _class.instanceMethodId( + r'getSerializableExtra', + r'(Ljava/lang/String;Ljava/lang/Class;)Ljava/io/Serializable;', + ); + + static final _getSerializableExtra$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public T getSerializableExtra(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + $T? getSerializableExtra$1<$T extends jni$_.JObject?>(jni$_.JString? string, jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getSerializableExtra$1(reference.pointer, _id_getSerializableExtra$1 as jni$_.JMethodIDPtr, _$string.pointer, _$class$.pointer).object<$T?>(T.nullableType); + } + + static final _id_getShortArrayExtra = _class.instanceMethodId( + r'getShortArrayExtra', + r'(Ljava/lang/String;)[S', + ); + + static final _getShortArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public short[] getShortArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JShortArray? getShortArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getShortArrayExtra(reference.pointer, _id_getShortArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JShortArray$NullableType$()); + } + + static final _id_getShortExtra = _class.instanceMethodId( + r'getShortExtra', + r'(Ljava/lang/String;S)S', + ); + + static final _getShortExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallShortMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public short getShortExtra(java.lang.String string, short s)` + int getShortExtra(jni$_.JString? string, int s, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getShortExtra(reference.pointer, _id_getShortExtra as jni$_.JMethodIDPtr, _$string.pointer, s).short; + } + + static final _id_getSourceBounds = _class.instanceMethodId( + r'getSourceBounds', + r'()Landroid/graphics/Rect;', + ); + + static final _getSourceBounds = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.graphics.Rect getSourceBounds()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSourceBounds(){ + + + return _getSourceBounds(reference.pointer, _id_getSourceBounds as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getStringArrayExtra = _class.instanceMethodId( + r'getStringArrayExtra', + r'(Ljava/lang/String;)[Ljava/lang/String;', + ); + + static final _getStringArrayExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.lang.String[] getStringArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getStringArrayExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getStringArrayExtra(reference.pointer, _id_getStringArrayExtra as jni$_.JMethodIDPtr, _$string.pointer).object?>(const jni$_.$JArray$NullableType$(jni$_.$JString$NullableType$())); + } + + static final _id_getStringArrayListExtra = _class.instanceMethodId( + r'getStringArrayListExtra', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getStringArrayListExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.util.ArrayList getStringArrayListExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getStringArrayListExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getStringArrayListExtra(reference.pointer, _id_getStringArrayListExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getStringExtra = _class.instanceMethodId( + r'getStringExtra', + r'(Ljava/lang/String;)Ljava/lang/String;', + ); + + static final _getStringExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.lang.String getStringExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getStringExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getStringExtra(reference.pointer, _id_getStringExtra as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getType = _class.instanceMethodId( + r'getType', + r'()Ljava/lang/String;', + ); + + static final _getType = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String getType()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getType(){ + + + return _getType(reference.pointer, _id_getType as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_hasCategory = _class.instanceMethodId( + r'hasCategory', + r'(Ljava/lang/String;)Z', + ); + + static final _hasCategory = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public boolean hasCategory(java.lang.String string)` + bool hasCategory(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _hasCategory(reference.pointer, _id_hasCategory as jni$_.JMethodIDPtr, _$string.pointer).boolean; + } + + static final _id_hasExtra = _class.instanceMethodId( + r'hasExtra', + r'(Ljava/lang/String;)Z', + ); + + static final _hasExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public boolean hasExtra(java.lang.String string)` + bool hasExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _hasExtra(reference.pointer, _id_hasExtra as jni$_.JMethodIDPtr, _$string.pointer).boolean; + } + + static final _id_hasFileDescriptors = _class.instanceMethodId( + r'hasFileDescriptors', + r'()Z', + ); + + static final _hasFileDescriptors = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public boolean hasFileDescriptors()` + bool hasFileDescriptors(){ + + + return _hasFileDescriptors(reference.pointer, _id_hasFileDescriptors as jni$_.JMethodIDPtr).boolean; + } + + static final _id_isMismatchingFilter = _class.instanceMethodId( + r'isMismatchingFilter', + r'()Z', + ); + + static final _isMismatchingFilter = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public boolean isMismatchingFilter()` + bool isMismatchingFilter(){ + + + return _isMismatchingFilter(reference.pointer, _id_isMismatchingFilter as jni$_.JMethodIDPtr).boolean; + } + + static final _id_makeMainActivity = _class.staticMethodId( + r'makeMainActivity', + r'(Landroid/content/ComponentName;)Landroid/content/Intent;', + ); + + static final _makeMainActivity = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `static public android.content.Intent makeMainActivity(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? makeMainActivity(jni$_.JObject? componentName, ){ + + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _makeMainActivity(_class.reference.pointer, _id_makeMainActivity as jni$_.JMethodIDPtr, _$componentName.pointer).object(const $Intent$NullableType$()); + } + + static final _id_makeMainSelectorActivity = _class.staticMethodId( + r'makeMainSelectorActivity', + r'(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _makeMainSelectorActivity = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `static public android.content.Intent makeMainSelectorActivity(java.lang.String string, java.lang.String string1)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? makeMainSelectorActivity(jni$_.JString? string, jni$_.JString? string1, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _makeMainSelectorActivity(_class.reference.pointer, _id_makeMainSelectorActivity as jni$_.JMethodIDPtr, _$string.pointer, _$string1.pointer).object(const $Intent$NullableType$()); + } + + static final _id_makeRestartActivityTask = _class.staticMethodId( + r'makeRestartActivityTask', + r'(Landroid/content/ComponentName;)Landroid/content/Intent;', + ); + + static final _makeRestartActivityTask = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `static public android.content.Intent makeRestartActivityTask(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? makeRestartActivityTask(jni$_.JObject? componentName, ){ + + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _makeRestartActivityTask(_class.reference.pointer, _id_makeRestartActivityTask as jni$_.JMethodIDPtr, _$componentName.pointer).object(const $Intent$NullableType$()); + } + + static final _id_normalizeMimeType = _class.staticMethodId( + r'normalizeMimeType', + r'(Ljava/lang/String;)Ljava/lang/String;', + ); + + static final _normalizeMimeType = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `static public java.lang.String normalizeMimeType(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? normalizeMimeType(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _normalizeMimeType(_class.reference.pointer, _id_normalizeMimeType as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JString$NullableType$()); + } + + static final _id_parseIntent = _class.staticMethodId( + r'parseIntent', + r'(Landroid/content/res/Resources;Lorg/xmlpull/v1/XmlPullParser;Landroid/util/AttributeSet;)Landroid/content/Intent;', + ); + + static final _parseIntent = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `static public android.content.Intent parseIntent(android.content.res.Resources resources, org.xmlpull.v1.XmlPullParser xmlPullParser, android.util.AttributeSet attributeSet)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? parseIntent(jni$_.JObject? resources, jni$_.JObject? xmlPullParser, jni$_.JObject? attributeSet, ){ + + final _$resources = resources?.reference ?? jni$_.jNullReference; + final _$xmlPullParser = xmlPullParser?.reference ?? jni$_.jNullReference; + final _$attributeSet = attributeSet?.reference ?? jni$_.jNullReference; + return _parseIntent(_class.reference.pointer, _id_parseIntent as jni$_.JMethodIDPtr, _$resources.pointer, _$xmlPullParser.pointer, _$attributeSet.pointer).object(const $Intent$NullableType$()); + } + + static final _id_parseUri = _class.staticMethodId( + r'parseUri', + r'(Ljava/lang/String;I)Landroid/content/Intent;', + ); + + static final _parseUri = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `static public android.content.Intent parseUri(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? parseUri(jni$_.JString? string, int i, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _parseUri(_class.reference.pointer, _id_parseUri as jni$_.JMethodIDPtr, _$string.pointer, i).object(const $Intent$NullableType$()); + } + + static final _id_putCharSequenceArrayListExtra = _class.instanceMethodId( + r'putCharSequenceArrayListExtra', + r'(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;', + ); + + static final _putCharSequenceArrayListExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putCharSequenceArrayListExtra(java.lang.String string, java.util.ArrayList arrayList)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putCharSequenceArrayListExtra(jni$_.JString? string, jni$_.JObject? arrayList, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + return _putCharSequenceArrayListExtra(reference.pointer, _id_putCharSequenceArrayListExtra as jni$_.JMethodIDPtr, _$string.pointer, _$arrayList.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Landroid/os/Bundle;)Landroid/content/Intent;', + ); + + static final _putExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, android.os.Bundle bundle)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra(jni$_.JString? string, Bundle? bundle, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _putExtra(reference.pointer, _id_putExtra as jni$_.JMethodIDPtr, _$string.pointer, _$bundle.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$1 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;', + ); + + static final _putExtra$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, android.os.Parcelable parcelable)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$1(jni$_.JString? string, jni$_.JObject? parcelable, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$parcelable = parcelable?.reference ?? jni$_.jNullReference; + return _putExtra$1(reference.pointer, _id_putExtra$1 as jni$_.JMethodIDPtr, _$string.pointer, _$parcelable.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$2 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[Landroid/os/Parcelable;)Landroid/content/Intent;', + ); + + static final _putExtra$2 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, android.os.Parcelable[] parcelables)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$2(jni$_.JString? string, jni$_.JArray? parcelables, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$parcelables = parcelables?.reference ?? jni$_.jNullReference; + return _putExtra$2(reference.pointer, _id_putExtra$2 as jni$_.JMethodIDPtr, _$string.pointer, _$parcelables.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$3 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Z)Landroid/content/Intent;', + ); + + static final _putExtra$3 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, boolean z)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$3(jni$_.JString? string, bool z, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$3(reference.pointer, _id_putExtra$3 as jni$_.JMethodIDPtr, _$string.pointer, z ? 1 : 0).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$4 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[Z)Landroid/content/Intent;', + ); + + static final _putExtra$4 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, boolean[] zs)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$4(jni$_.JString? string, jni$_.JBooleanArray? zs, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$zs = zs?.reference ?? jni$_.jNullReference; + return _putExtra$4(reference.pointer, _id_putExtra$4 as jni$_.JMethodIDPtr, _$string.pointer, _$zs.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$5 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;B)Landroid/content/Intent;', + ); + + static final _putExtra$5 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, byte b)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$5(jni$_.JString? string, int b, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$5(reference.pointer, _id_putExtra$5 as jni$_.JMethodIDPtr, _$string.pointer, b).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$6 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[B)Landroid/content/Intent;', + ); + + static final _putExtra$6 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, byte[] bs)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$6(jni$_.JString? string, jni$_.JByteArray? bs, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bs = bs?.reference ?? jni$_.jNullReference; + return _putExtra$6(reference.pointer, _id_putExtra$6 as jni$_.JMethodIDPtr, _$string.pointer, _$bs.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$7 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;C)Landroid/content/Intent;', + ); + + static final _putExtra$7 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, char c)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$7(jni$_.JString? string, int c, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$7(reference.pointer, _id_putExtra$7 as jni$_.JMethodIDPtr, _$string.pointer, c).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$8 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[C)Landroid/content/Intent;', + ); + + static final _putExtra$8 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, char[] cs)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$8(jni$_.JString? string, jni$_.JCharArray? cs, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$cs = cs?.reference ?? jni$_.jNullReference; + return _putExtra$8(reference.pointer, _id_putExtra$8 as jni$_.JMethodIDPtr, _$string.pointer, _$cs.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$9 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;D)Landroid/content/Intent;', + ); + + static final _putExtra$9 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, double)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, double d)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$9(jni$_.JString? string, double d, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$9(reference.pointer, _id_putExtra$9 as jni$_.JMethodIDPtr, _$string.pointer, d).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$10 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[D)Landroid/content/Intent;', + ); + + static final _putExtra$10 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, double[] ds)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$10(jni$_.JString? string, jni$_.JDoubleArray? ds, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$ds = ds?.reference ?? jni$_.jNullReference; + return _putExtra$10(reference.pointer, _id_putExtra$10 as jni$_.JMethodIDPtr, _$string.pointer, _$ds.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$11 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;F)Landroid/content/Intent;', + ); + + static final _putExtra$11 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, double)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, float f)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$11(jni$_.JString? string, double f, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$11(reference.pointer, _id_putExtra$11 as jni$_.JMethodIDPtr, _$string.pointer, f).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$12 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[F)Landroid/content/Intent;', + ); + + static final _putExtra$12 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, float[] fs)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$12(jni$_.JString? string, jni$_.JFloatArray? fs, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$fs = fs?.reference ?? jni$_.jNullReference; + return _putExtra$12(reference.pointer, _id_putExtra$12 as jni$_.JMethodIDPtr, _$string.pointer, _$fs.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$13 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;I)Landroid/content/Intent;', + ); + + static final _putExtra$13 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$13(jni$_.JString? string, int i, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$13(reference.pointer, _id_putExtra$13 as jni$_.JMethodIDPtr, _$string.pointer, i).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$14 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[I)Landroid/content/Intent;', + ); + + static final _putExtra$14 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, int[] is)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$14(jni$_.JString? string, jni$_.JIntArray? is$, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$is$ = is$?.reference ?? jni$_.jNullReference; + return _putExtra$14(reference.pointer, _id_putExtra$14 as jni$_.JMethodIDPtr, _$string.pointer, _$is$.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$15 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Ljava/io/Serializable;)Landroid/content/Intent;', + ); + + static final _putExtra$15 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, java.io.Serializable serializable)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$15(jni$_.JString? string, jni$_.JObject? serializable, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$serializable = serializable?.reference ?? jni$_.jNullReference; + return _putExtra$15(reference.pointer, _id_putExtra$15 as jni$_.JMethodIDPtr, _$string.pointer, _$serializable.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$16 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Ljava/lang/CharSequence;)Landroid/content/Intent;', + ); + + static final _putExtra$16 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, java.lang.CharSequence charSequence)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$16(jni$_.JString? string, jni$_.JObject? charSequence, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + return _putExtra$16(reference.pointer, _id_putExtra$16 as jni$_.JMethodIDPtr, _$string.pointer, _$charSequence.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$17 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[Ljava/lang/CharSequence;)Landroid/content/Intent;', + ); + + static final _putExtra$17 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, java.lang.CharSequence[] charSequences)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$17(jni$_.JString? string, jni$_.JArray? charSequences, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$charSequences = charSequences?.reference ?? jni$_.jNullReference; + return _putExtra$17(reference.pointer, _id_putExtra$17 as jni$_.JMethodIDPtr, _$string.pointer, _$charSequences.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$18 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _putExtra$18 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, java.lang.String string1)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$18(jni$_.JString? string, jni$_.JString? string1, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _putExtra$18(reference.pointer, _id_putExtra$18 as jni$_.JMethodIDPtr, _$string.pointer, _$string1.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$19 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _putExtra$19 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, java.lang.String[] strings)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$19(jni$_.JString? string, jni$_.JArray? strings, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$strings = strings?.reference ?? jni$_.jNullReference; + return _putExtra$19(reference.pointer, _id_putExtra$19 as jni$_.JMethodIDPtr, _$string.pointer, _$strings.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$20 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;J)Landroid/content/Intent;', + ); + + static final _putExtra$20 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int64)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, long j)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$20(jni$_.JString? string, int j, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$20(reference.pointer, _id_putExtra$20 as jni$_.JMethodIDPtr, _$string.pointer, j).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$21 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[J)Landroid/content/Intent;', + ); + + static final _putExtra$21 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, long[] js)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$21(jni$_.JString? string, jni$_.JLongArray? js, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$js = js?.reference ?? jni$_.jNullReference; + return _putExtra$21(reference.pointer, _id_putExtra$21 as jni$_.JMethodIDPtr, _$string.pointer, _$js.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$22 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;S)Landroid/content/Intent;', + ); + + static final _putExtra$22 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, short s)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$22(jni$_.JString? string, int s, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$22(reference.pointer, _id_putExtra$22 as jni$_.JMethodIDPtr, _$string.pointer, s).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$23 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[S)Landroid/content/Intent;', + ); + + static final _putExtra$23 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtra(java.lang.String string, short[] ss)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$23(jni$_.JString? string, jni$_.JShortArray? ss, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$ss = ss?.reference ?? jni$_.jNullReference; + return _putExtra$23(reference.pointer, _id_putExtra$23 as jni$_.JMethodIDPtr, _$string.pointer, _$ss.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtras = _class.instanceMethodId( + r'putExtras', + r'(Landroid/content/Intent;)Landroid/content/Intent;', + ); + + static final _putExtras = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtras(android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtras(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _putExtras(reference.pointer, _id_putExtras as jni$_.JMethodIDPtr, _$intent.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putExtras$1 = _class.instanceMethodId( + r'putExtras', + r'(Landroid/os/Bundle;)Landroid/content/Intent;', + ); + + static final _putExtras$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent putExtras(android.os.Bundle bundle)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtras$1(Bundle? bundle, ){ + + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _putExtras$1(reference.pointer, _id_putExtras$1 as jni$_.JMethodIDPtr, _$bundle.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putIntegerArrayListExtra = _class.instanceMethodId( + r'putIntegerArrayListExtra', + r'(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;', + ); + + static final _putIntegerArrayListExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putIntegerArrayListExtra(java.lang.String string, java.util.ArrayList arrayList)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putIntegerArrayListExtra(jni$_.JString? string, jni$_.JObject? arrayList, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + return _putIntegerArrayListExtra(reference.pointer, _id_putIntegerArrayListExtra as jni$_.JMethodIDPtr, _$string.pointer, _$arrayList.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putParcelableArrayListExtra = _class.instanceMethodId( + r'putParcelableArrayListExtra', + r'(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;', + ); + + static final _putParcelableArrayListExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putParcelableArrayListExtra(java.lang.String string, java.util.ArrayList arrayList)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putParcelableArrayListExtra(jni$_.JString? string, jni$_.JObject? arrayList, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + return _putParcelableArrayListExtra(reference.pointer, _id_putParcelableArrayListExtra as jni$_.JMethodIDPtr, _$string.pointer, _$arrayList.pointer).object(const $Intent$NullableType$()); + } + + static final _id_putStringArrayListExtra = _class.instanceMethodId( + r'putStringArrayListExtra', + r'(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;', + ); + + static final _putStringArrayListExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent putStringArrayListExtra(java.lang.String string, java.util.ArrayList arrayList)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putStringArrayListExtra(jni$_.JString? string, jni$_.JObject? arrayList, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + return _putStringArrayListExtra(reference.pointer, _id_putStringArrayListExtra as jni$_.JMethodIDPtr, _$string.pointer, _$arrayList.pointer).object(const $Intent$NullableType$()); + } + + static final _id_readFromParcel = _class.instanceMethodId( + r'readFromParcel', + r'(Landroid/os/Parcel;)V', + ); + + static final _readFromParcel = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void readFromParcel(android.os.Parcel parcel)` + void readFromParcel(jni$_.JObject? parcel, ){ + + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _readFromParcel(reference.pointer, _id_readFromParcel as jni$_.JMethodIDPtr, _$parcel.pointer).check(); + } + + static final _id_removeCategory = _class.instanceMethodId( + r'removeCategory', + r'(Ljava/lang/String;)V', + ); + + static final _removeCategory = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void removeCategory(java.lang.String string)` + void removeCategory(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + _removeCategory(reference.pointer, _id_removeCategory as jni$_.JMethodIDPtr, _$string.pointer).check(); + } + + static final _id_removeExtra = _class.instanceMethodId( + r'removeExtra', + r'(Ljava/lang/String;)V', + ); + + static final _removeExtra = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void removeExtra(java.lang.String string)` + void removeExtra(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + _removeExtra(reference.pointer, _id_removeExtra as jni$_.JMethodIDPtr, _$string.pointer).check(); + } + + static final _id_removeFlags = _class.instanceMethodId( + r'removeFlags', + r'(I)V', + ); + + static final _removeFlags = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public void removeFlags(int i)` + void removeFlags(int i, ){ + + + _removeFlags(reference.pointer, _id_removeFlags as jni$_.JMethodIDPtr, i).check(); + } + + static final _id_removeLaunchSecurityProtection = _class.instanceMethodId( + r'removeLaunchSecurityProtection', + r'()V', + ); + + static final _removeLaunchSecurityProtection = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void removeLaunchSecurityProtection()` + void removeLaunchSecurityProtection(){ + + + _removeLaunchSecurityProtection(reference.pointer, _id_removeLaunchSecurityProtection as jni$_.JMethodIDPtr).check(); + } + + static final _id_replaceExtras = _class.instanceMethodId( + r'replaceExtras', + r'(Landroid/content/Intent;)Landroid/content/Intent;', + ); + + static final _replaceExtras = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent replaceExtras(android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + Intent? replaceExtras(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _replaceExtras(reference.pointer, _id_replaceExtras as jni$_.JMethodIDPtr, _$intent.pointer).object(const $Intent$NullableType$()); + } + + static final _id_replaceExtras$1 = _class.instanceMethodId( + r'replaceExtras', + r'(Landroid/os/Bundle;)Landroid/content/Intent;', + ); + + static final _replaceExtras$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent replaceExtras(android.os.Bundle bundle)` + /// The returned object must be released after use, by calling the [release] method. + Intent? replaceExtras$1(Bundle? bundle, ){ + + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _replaceExtras$1(reference.pointer, _id_replaceExtras$1 as jni$_.JMethodIDPtr, _$bundle.pointer).object(const $Intent$NullableType$()); + } + + static final _id_resolveActivity = _class.instanceMethodId( + r'resolveActivity', + r'(Landroid/content/pm/PackageManager;)Landroid/content/ComponentName;', + ); + + static final _resolveActivity = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.ComponentName resolveActivity(android.content.pm.PackageManager packageManager)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveActivity(jni$_.JObject? packageManager, ){ + + final _$packageManager = packageManager?.reference ?? jni$_.jNullReference; + return _resolveActivity(reference.pointer, _id_resolveActivity as jni$_.JMethodIDPtr, _$packageManager.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_resolveActivityInfo = _class.instanceMethodId( + r'resolveActivityInfo', + r'(Landroid/content/pm/PackageManager;I)Landroid/content/pm/ActivityInfo;', + ); + + static final _resolveActivityInfo = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public android.content.pm.ActivityInfo resolveActivityInfo(android.content.pm.PackageManager packageManager, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveActivityInfo(jni$_.JObject? packageManager, int i, ){ + + final _$packageManager = packageManager?.reference ?? jni$_.jNullReference; + return _resolveActivityInfo(reference.pointer, _id_resolveActivityInfo as jni$_.JMethodIDPtr, _$packageManager.pointer, i).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_resolveType = _class.instanceMethodId( + r'resolveType', + r'(Landroid/content/ContentResolver;)Ljava/lang/String;', + ); + + static final _resolveType = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.lang.String resolveType(android.content.ContentResolver contentResolver)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? resolveType(jni$_.JObject? contentResolver, ){ + + final _$contentResolver = contentResolver?.reference ?? jni$_.jNullReference; + return _resolveType(reference.pointer, _id_resolveType as jni$_.JMethodIDPtr, _$contentResolver.pointer).object(const jni$_.$JString$NullableType$()); + } + + static final _id_resolveType$1 = _class.instanceMethodId( + r'resolveType', + r'(Landroid/content/Context;)Ljava/lang/String;', + ); + + static final _resolveType$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.lang.String resolveType(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? resolveType$1(Context? context, ){ + + final _$context = context?.reference ?? jni$_.jNullReference; + return _resolveType$1(reference.pointer, _id_resolveType$1 as jni$_.JMethodIDPtr, _$context.pointer).object(const jni$_.$JString$NullableType$()); + } + + static final _id_resolveTypeIfNeeded = _class.instanceMethodId( + r'resolveTypeIfNeeded', + r'(Landroid/content/ContentResolver;)Ljava/lang/String;', + ); + + static final _resolveTypeIfNeeded = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public java.lang.String resolveTypeIfNeeded(android.content.ContentResolver contentResolver)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? resolveTypeIfNeeded(jni$_.JObject? contentResolver, ){ + + final _$contentResolver = contentResolver?.reference ?? jni$_.jNullReference; + return _resolveTypeIfNeeded(reference.pointer, _id_resolveTypeIfNeeded as jni$_.JMethodIDPtr, _$contentResolver.pointer).object(const jni$_.$JString$NullableType$()); + } + + static final _id_setAction = _class.instanceMethodId( + r'setAction', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setAction = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent setAction(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setAction(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _setAction(reference.pointer, _id_setAction as jni$_.JMethodIDPtr, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setClass = _class.instanceMethodId( + r'setClass', + r'(Landroid/content/Context;Ljava/lang/Class;)Landroid/content/Intent;', + ); + + static final _setClass = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent setClass(android.content.Context context, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setClass(Context? context, jni$_.JObject? class$, ){ + + final _$context = context?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _setClass(reference.pointer, _id_setClass as jni$_.JMethodIDPtr, _$context.pointer, _$class$.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setClassName = _class.instanceMethodId( + r'setClassName', + r'(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setClassName = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent setClassName(android.content.Context context, java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setClassName(Context? context, jni$_.JString? string, ){ + + final _$context = context?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _setClassName(reference.pointer, _id_setClassName as jni$_.JMethodIDPtr, _$context.pointer, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setClassName$1 = _class.instanceMethodId( + r'setClassName', + r'(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setClassName$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent setClassName(java.lang.String string, java.lang.String string1)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setClassName$1(jni$_.JString? string, jni$_.JString? string1, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _setClassName$1(reference.pointer, _id_setClassName$1 as jni$_.JMethodIDPtr, _$string.pointer, _$string1.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setClipData = _class.instanceMethodId( + r'setClipData', + r'(Landroid/content/ClipData;)V', + ); + + static final _setClipData = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void setClipData(android.content.ClipData clipData)` + void setClipData(jni$_.JObject? clipData, ){ + + final _$clipData = clipData?.reference ?? jni$_.jNullReference; + _setClipData(reference.pointer, _id_setClipData as jni$_.JMethodIDPtr, _$clipData.pointer).check(); + } + + static final _id_setComponent = _class.instanceMethodId( + r'setComponent', + r'(Landroid/content/ComponentName;)Landroid/content/Intent;', + ); + + static final _setComponent = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent setComponent(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setComponent(jni$_.JObject? componentName, ){ + + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _setComponent(reference.pointer, _id_setComponent as jni$_.JMethodIDPtr, _$componentName.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setData = _class.instanceMethodId( + r'setData', + r'(Landroid/net/Uri;)Landroid/content/Intent;', + ); + + static final _setData = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent setData(android.net.Uri uri)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setData(jni$_.JObject? uri, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + return _setData(reference.pointer, _id_setData as jni$_.JMethodIDPtr, _$uri.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setDataAndNormalize = _class.instanceMethodId( + r'setDataAndNormalize', + r'(Landroid/net/Uri;)Landroid/content/Intent;', + ); + + static final _setDataAndNormalize = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent setDataAndNormalize(android.net.Uri uri)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setDataAndNormalize(jni$_.JObject? uri, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + return _setDataAndNormalize(reference.pointer, _id_setDataAndNormalize as jni$_.JMethodIDPtr, _$uri.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setDataAndType = _class.instanceMethodId( + r'setDataAndType', + r'(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setDataAndType = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent setDataAndType(android.net.Uri uri, java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setDataAndType(jni$_.JObject? uri, jni$_.JString? string, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _setDataAndType(reference.pointer, _id_setDataAndType as jni$_.JMethodIDPtr, _$uri.pointer, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setDataAndTypeAndNormalize = _class.instanceMethodId( + r'setDataAndTypeAndNormalize', + r'(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setDataAndTypeAndNormalize = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public android.content.Intent setDataAndTypeAndNormalize(android.net.Uri uri, java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setDataAndTypeAndNormalize(jni$_.JObject? uri, jni$_.JString? string, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _setDataAndTypeAndNormalize(reference.pointer, _id_setDataAndTypeAndNormalize as jni$_.JMethodIDPtr, _$uri.pointer, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setExtrasClassLoader = _class.instanceMethodId( + r'setExtrasClassLoader', + r'(Ljava/lang/ClassLoader;)V', + ); + + static final _setExtrasClassLoader = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void setExtrasClassLoader(java.lang.ClassLoader classLoader)` + void setExtrasClassLoader(jni$_.JObject? classLoader, ){ + + final _$classLoader = classLoader?.reference ?? jni$_.jNullReference; + _setExtrasClassLoader(reference.pointer, _id_setExtrasClassLoader as jni$_.JMethodIDPtr, _$classLoader.pointer).check(); + } + + static final _id_setFlags = _class.instanceMethodId( + r'setFlags', + r'(I)Landroid/content/Intent;', + ); + + static final _setFlags = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public android.content.Intent setFlags(int i)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setFlags(int i, ){ + + + return _setFlags(reference.pointer, _id_setFlags as jni$_.JMethodIDPtr, i).object(const $Intent$NullableType$()); + } + + static final _id_setIdentifier = _class.instanceMethodId( + r'setIdentifier', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setIdentifier = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent setIdentifier(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setIdentifier(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _setIdentifier(reference.pointer, _id_setIdentifier as jni$_.JMethodIDPtr, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setPackage = _class.instanceMethodId( + r'setPackage', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setPackage = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent setPackage(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setPackage(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _setPackage(reference.pointer, _id_setPackage as jni$_.JMethodIDPtr, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setSelector = _class.instanceMethodId( + r'setSelector', + r'(Landroid/content/Intent;)V', + ); + + static final _setSelector = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void setSelector(android.content.Intent intent)` + void setSelector(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + _setSelector(reference.pointer, _id_setSelector as jni$_.JMethodIDPtr, _$intent.pointer).check(); + } + + static final _id_setSourceBounds = _class.instanceMethodId( + r'setSourceBounds', + r'(Landroid/graphics/Rect;)V', + ); + + static final _setSourceBounds = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void setSourceBounds(android.graphics.Rect rect)` + void setSourceBounds(jni$_.JObject? rect, ){ + + final _$rect = rect?.reference ?? jni$_.jNullReference; + _setSourceBounds(reference.pointer, _id_setSourceBounds as jni$_.JMethodIDPtr, _$rect.pointer).check(); + } + + static final _id_setType = _class.instanceMethodId( + r'setType', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setType = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent setType(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setType(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _setType(reference.pointer, _id_setType as jni$_.JMethodIDPtr, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_setTypeAndNormalize = _class.instanceMethodId( + r'setTypeAndNormalize', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setTypeAndNormalize = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Intent setTypeAndNormalize(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setTypeAndNormalize(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _setTypeAndNormalize(reference.pointer, _id_setTypeAndNormalize as jni$_.JMethodIDPtr, _$string.pointer).object(const $Intent$NullableType$()); + } + + static final _id_toString$1 = _class.instanceMethodId( + r'toString', + r'()Ljava/lang/String;', + ); + + static final _toString$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String toString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toString$1(){ + + + return _toString$1(reference.pointer, _id_toString$1 as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_toURI = _class.instanceMethodId( + r'toURI', + r'()Ljava/lang/String;', + ); + + static final _toURI = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String toURI()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toURI(){ + + + return _toURI(reference.pointer, _id_toURI as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_toUri = _class.instanceMethodId( + r'toUri', + r'(I)Ljava/lang/String;', + ); + + static final _toUri = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public java.lang.String toUri(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toUri(int i, ){ + + + return _toUri(reference.pointer, _id_toUri as jni$_.JMethodIDPtr, i).object(const jni$_.$JString$NullableType$()); + } + + static final _id_writeToParcel = _class.instanceMethodId( + r'writeToParcel', + r'(Landroid/os/Parcel;I)V', + ); + + static final _writeToParcel = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public void writeToParcel(android.os.Parcel parcel, int i)` + void writeToParcel(jni$_.JObject? parcel, int i, ){ + + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _writeToParcel(reference.pointer, _id_writeToParcel as jni$_.JMethodIDPtr, _$parcel.pointer, i).check(); + } + +} +final class $Intent$NullableType$ extends jni$_.JType { + + + @jni$_.internal + const $Intent$NullableType$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent;'; + + @jni$_.internal + @core$_.override + Intent? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Intent.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$NullableType$) && + other is $Intent$NullableType$; + } +} + +final class $Intent$Type$ extends jni$_.JType { + + + @jni$_.internal + const $Intent$Type$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent;'; + + @jni$_.internal + @core$_.override + Intent fromReference(jni$_.JReference reference) => + Intent.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Intent$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$Type$) && + other is $Intent$Type$; + } +} + +/// from: `android.content.Context$BindServiceFlags` +class Context$BindServiceFlags extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + + + @jni$_.internal + Context$BindServiceFlags.fromReference( + + jni$_.JReference reference, + ) : + $type = type, + super.fromReference( + + reference + ); + + static final _class = jni$_.JClass.forName(r'android/content/Context$BindServiceFlags'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Context$BindServiceFlags$NullableType$(); + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Context$BindServiceFlags$Type$(); + static final _id_of = _class.staticMethodId( + r'of', + r'(J)Landroid/content/Context$BindServiceFlags;', + ); + + static final _of = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int64,)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `static public android.content.Context$BindServiceFlags of(long j)` + /// The returned object must be released after use, by calling the [release] method. + static Context$BindServiceFlags? of(int j, ){ + + + return _of(_class.reference.pointer, _id_of as jni$_.JMethodIDPtr, j).object(const $Context$BindServiceFlags$NullableType$()); + } + +} +final class $Context$BindServiceFlags$NullableType$ extends jni$_.JType { + + + @jni$_.internal + const $Context$BindServiceFlags$NullableType$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Context$BindServiceFlags;'; + + @jni$_.internal + @core$_.override + Context$BindServiceFlags? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Context$BindServiceFlags.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Context$BindServiceFlags$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Context$BindServiceFlags$NullableType$) && + other is $Context$BindServiceFlags$NullableType$; + } +} + +final class $Context$BindServiceFlags$Type$ extends jni$_.JType { + + + @jni$_.internal + const $Context$BindServiceFlags$Type$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Context$BindServiceFlags;'; + + @jni$_.internal + @core$_.override + Context$BindServiceFlags fromReference(jni$_.JReference reference) => + Context$BindServiceFlags.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Context$BindServiceFlags$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Context$BindServiceFlags$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Context$BindServiceFlags$Type$) && + other is $Context$BindServiceFlags$Type$; + } +} + +/// from: `android.content.Context` +class Context extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + + + @jni$_.internal + Context.fromReference( + + jni$_.JReference reference, + ) : + $type = type, + super.fromReference( + + reference + ); + + static final _class = jni$_.JClass.forName(r'android/content/Context'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Context$NullableType$(); + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Context$Type$(); + static final _id_ACCESSIBILITY_SERVICE = + _class.staticFieldId( + r'ACCESSIBILITY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACCESSIBILITY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACCESSIBILITY_SERVICE => _id_ACCESSIBILITY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACCOUNT_SERVICE = + _class.staticFieldId( + r'ACCOUNT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACCOUNT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACCOUNT_SERVICE => _id_ACCOUNT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTIVITY_SERVICE = + _class.staticFieldId( + r'ACTIVITY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ACTIVITY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ACTIVITY_SERVICE => _id_ACTIVITY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ADVANCED_PROTECTION_SERVICE = + _class.staticFieldId( + r'ADVANCED_PROTECTION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ADVANCED_PROTECTION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ADVANCED_PROTECTION_SERVICE => _id_ADVANCED_PROTECTION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ALARM_SERVICE = + _class.staticFieldId( + r'ALARM_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ALARM_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ALARM_SERVICE => _id_ALARM_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_APPWIDGET_SERVICE = + _class.staticFieldId( + r'APPWIDGET_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String APPWIDGET_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get APPWIDGET_SERVICE => _id_APPWIDGET_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_APP_FUNCTION_SERVICE = + _class.staticFieldId( + r'APP_FUNCTION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String APP_FUNCTION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get APP_FUNCTION_SERVICE => _id_APP_FUNCTION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_APP_OPS_SERVICE = + _class.staticFieldId( + r'APP_OPS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String APP_OPS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get APP_OPS_SERVICE => _id_APP_OPS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_APP_SEARCH_SERVICE = + _class.staticFieldId( + r'APP_SEARCH_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String APP_SEARCH_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get APP_SEARCH_SERVICE => _id_APP_SEARCH_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_AUDIO_SERVICE = + _class.staticFieldId( + r'AUDIO_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String AUDIO_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get AUDIO_SERVICE => _id_AUDIO_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BATTERY_SERVICE = + _class.staticFieldId( + r'BATTERY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String BATTERY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get BATTERY_SERVICE => _id_BATTERY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int BIND_ABOVE_CLIENT` + static const BIND_ABOVE_CLIENT = 8; + /// from: `static public final int BIND_ADJUST_WITH_ACTIVITY` + static const BIND_ADJUST_WITH_ACTIVITY = 128; + /// from: `static public final int BIND_ALLOW_ACTIVITY_STARTS` + static const BIND_ALLOW_ACTIVITY_STARTS = 512; + /// from: `static public final int BIND_ALLOW_OOM_MANAGEMENT` + static const BIND_ALLOW_OOM_MANAGEMENT = 16; + /// from: `static public final int BIND_AUTO_CREATE` + static const BIND_AUTO_CREATE = 1; + /// from: `static public final int BIND_DEBUG_UNBIND` + static const BIND_DEBUG_UNBIND = 2; + /// from: `static public final int BIND_EXTERNAL_SERVICE` + static const BIND_EXTERNAL_SERVICE = -2147483648; + /// from: `static public final long BIND_EXTERNAL_SERVICE_LONG` + static const BIND_EXTERNAL_SERVICE_LONG = 4611686018427387904; + /// from: `static public final int BIND_IMPORTANT` + static const BIND_IMPORTANT = 64; + /// from: `static public final int BIND_INCLUDE_CAPABILITIES` + static const BIND_INCLUDE_CAPABILITIES = 4096; + /// from: `static public final int BIND_NOT_FOREGROUND` + static const BIND_NOT_FOREGROUND = 4; + /// from: `static public final int BIND_NOT_PERCEPTIBLE` + static const BIND_NOT_PERCEPTIBLE = 256; + /// from: `static public final int BIND_PACKAGE_ISOLATED_PROCESS` + static const BIND_PACKAGE_ISOLATED_PROCESS = 16384; + /// from: `static public final int BIND_SHARED_ISOLATED_PROCESS` + static const BIND_SHARED_ISOLATED_PROCESS = 8192; + /// from: `static public final int BIND_WAIVE_PRIORITY` + static const BIND_WAIVE_PRIORITY = 32; + static final _id_BIOMETRIC_SERVICE = + _class.staticFieldId( + r'BIOMETRIC_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String BIOMETRIC_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get BIOMETRIC_SERVICE => _id_BIOMETRIC_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BLOB_STORE_SERVICE = + _class.staticFieldId( + r'BLOB_STORE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String BLOB_STORE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get BLOB_STORE_SERVICE => _id_BLOB_STORE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BLUETOOTH_SERVICE = + _class.staticFieldId( + r'BLUETOOTH_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String BLUETOOTH_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get BLUETOOTH_SERVICE => _id_BLUETOOTH_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BUGREPORT_SERVICE = + _class.staticFieldId( + r'BUGREPORT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String BUGREPORT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get BUGREPORT_SERVICE => _id_BUGREPORT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CAMERA_SERVICE = + _class.staticFieldId( + r'CAMERA_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CAMERA_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CAMERA_SERVICE => _id_CAMERA_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CAPTIONING_SERVICE = + _class.staticFieldId( + r'CAPTIONING_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CAPTIONING_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CAPTIONING_SERVICE => _id_CAPTIONING_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CARRIER_CONFIG_SERVICE = + _class.staticFieldId( + r'CARRIER_CONFIG_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CARRIER_CONFIG_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CARRIER_CONFIG_SERVICE => _id_CARRIER_CONFIG_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CLIPBOARD_SERVICE = + _class.staticFieldId( + r'CLIPBOARD_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CLIPBOARD_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CLIPBOARD_SERVICE => _id_CLIPBOARD_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_COMPANION_DEVICE_SERVICE = + _class.staticFieldId( + r'COMPANION_DEVICE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String COMPANION_DEVICE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get COMPANION_DEVICE_SERVICE => _id_COMPANION_DEVICE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CONNECTIVITY_DIAGNOSTICS_SERVICE = + _class.staticFieldId( + r'CONNECTIVITY_DIAGNOSTICS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CONNECTIVITY_DIAGNOSTICS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CONNECTIVITY_DIAGNOSTICS_SERVICE => _id_CONNECTIVITY_DIAGNOSTICS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CONNECTIVITY_SERVICE = + _class.staticFieldId( + r'CONNECTIVITY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CONNECTIVITY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CONNECTIVITY_SERVICE => _id_CONNECTIVITY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CONSUMER_IR_SERVICE = + _class.staticFieldId( + r'CONSUMER_IR_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CONSUMER_IR_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CONSUMER_IR_SERVICE => _id_CONSUMER_IR_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CONTACT_KEYS_SERVICE = + _class.staticFieldId( + r'CONTACT_KEYS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CONTACT_KEYS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CONTACT_KEYS_SERVICE => _id_CONTACT_KEYS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int CONTEXT_IGNORE_SECURITY` + static const CONTEXT_IGNORE_SECURITY = 2; + /// from: `static public final int CONTEXT_INCLUDE_CODE` + static const CONTEXT_INCLUDE_CODE = 1; + /// from: `static public final int CONTEXT_RESTRICTED` + static const CONTEXT_RESTRICTED = 4; + static final _id_CREDENTIAL_SERVICE = + _class.staticFieldId( + r'CREDENTIAL_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CREDENTIAL_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CREDENTIAL_SERVICE => _id_CREDENTIAL_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CROSS_PROFILE_APPS_SERVICE = + _class.staticFieldId( + r'CROSS_PROFILE_APPS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String CROSS_PROFILE_APPS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get CROSS_PROFILE_APPS_SERVICE => _id_CROSS_PROFILE_APPS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int DEVICE_ID_DEFAULT` + static const DEVICE_ID_DEFAULT = 0; + /// from: `static public final int DEVICE_ID_INVALID` + static const DEVICE_ID_INVALID = -1; + static final _id_DEVICE_LOCK_SERVICE = + _class.staticFieldId( + r'DEVICE_LOCK_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String DEVICE_LOCK_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get DEVICE_LOCK_SERVICE => _id_DEVICE_LOCK_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DEVICE_POLICY_SERVICE = + _class.staticFieldId( + r'DEVICE_POLICY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String DEVICE_POLICY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get DEVICE_POLICY_SERVICE => _id_DEVICE_POLICY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DISPLAY_HASH_SERVICE = + _class.staticFieldId( + r'DISPLAY_HASH_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String DISPLAY_HASH_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get DISPLAY_HASH_SERVICE => _id_DISPLAY_HASH_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DISPLAY_SERVICE = + _class.staticFieldId( + r'DISPLAY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String DISPLAY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get DISPLAY_SERVICE => _id_DISPLAY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DOMAIN_VERIFICATION_SERVICE = + _class.staticFieldId( + r'DOMAIN_VERIFICATION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String DOMAIN_VERIFICATION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get DOMAIN_VERIFICATION_SERVICE => _id_DOMAIN_VERIFICATION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DOWNLOAD_SERVICE = + _class.staticFieldId( + r'DOWNLOAD_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String DOWNLOAD_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get DOWNLOAD_SERVICE => _id_DOWNLOAD_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DROPBOX_SERVICE = + _class.staticFieldId( + r'DROPBOX_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String DROPBOX_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get DROPBOX_SERVICE => _id_DROPBOX_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EUICC_SERVICE = + _class.staticFieldId( + r'EUICC_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String EUICC_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get EUICC_SERVICE => _id_EUICC_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FILE_INTEGRITY_SERVICE = + _class.staticFieldId( + r'FILE_INTEGRITY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String FILE_INTEGRITY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get FILE_INTEGRITY_SERVICE => _id_FILE_INTEGRITY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FINGERPRINT_SERVICE = + _class.staticFieldId( + r'FINGERPRINT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String FINGERPRINT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get FINGERPRINT_SERVICE => _id_FINGERPRINT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_GAME_SERVICE = + _class.staticFieldId( + r'GAME_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String GAME_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get GAME_SERVICE => _id_GAME_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_GRAMMATICAL_INFLECTION_SERVICE = + _class.staticFieldId( + r'GRAMMATICAL_INFLECTION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String GRAMMATICAL_INFLECTION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get GRAMMATICAL_INFLECTION_SERVICE => _id_GRAMMATICAL_INFLECTION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_HARDWARE_PROPERTIES_SERVICE = + _class.staticFieldId( + r'HARDWARE_PROPERTIES_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String HARDWARE_PROPERTIES_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get HARDWARE_PROPERTIES_SERVICE => _id_HARDWARE_PROPERTIES_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_HEALTHCONNECT_SERVICE = + _class.staticFieldId( + r'HEALTHCONNECT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String HEALTHCONNECT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get HEALTHCONNECT_SERVICE => _id_HEALTHCONNECT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_INPUT_METHOD_SERVICE = + _class.staticFieldId( + r'INPUT_METHOD_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String INPUT_METHOD_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get INPUT_METHOD_SERVICE => _id_INPUT_METHOD_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_INPUT_SERVICE = + _class.staticFieldId( + r'INPUT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String INPUT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get INPUT_SERVICE => _id_INPUT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_IPSEC_SERVICE = + _class.staticFieldId( + r'IPSEC_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String IPSEC_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get IPSEC_SERVICE => _id_IPSEC_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_JOB_SCHEDULER_SERVICE = + _class.staticFieldId( + r'JOB_SCHEDULER_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String JOB_SCHEDULER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get JOB_SCHEDULER_SERVICE => _id_JOB_SCHEDULER_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_KEYGUARD_SERVICE = + _class.staticFieldId( + r'KEYGUARD_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String KEYGUARD_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get KEYGUARD_SERVICE => _id_KEYGUARD_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_KEYSTORE_SERVICE = + _class.staticFieldId( + r'KEYSTORE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String KEYSTORE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get KEYSTORE_SERVICE => _id_KEYSTORE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_LAUNCHER_APPS_SERVICE = + _class.staticFieldId( + r'LAUNCHER_APPS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String LAUNCHER_APPS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get LAUNCHER_APPS_SERVICE => _id_LAUNCHER_APPS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_LAYOUT_INFLATER_SERVICE = + _class.staticFieldId( + r'LAYOUT_INFLATER_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String LAYOUT_INFLATER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get LAYOUT_INFLATER_SERVICE => _id_LAYOUT_INFLATER_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_LOCALE_SERVICE = + _class.staticFieldId( + r'LOCALE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String LOCALE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get LOCALE_SERVICE => _id_LOCALE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_LOCATION_SERVICE = + _class.staticFieldId( + r'LOCATION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String LOCATION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get LOCATION_SERVICE => _id_LOCATION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MEDIA_COMMUNICATION_SERVICE = + _class.staticFieldId( + r'MEDIA_COMMUNICATION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String MEDIA_COMMUNICATION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get MEDIA_COMMUNICATION_SERVICE => _id_MEDIA_COMMUNICATION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MEDIA_METRICS_SERVICE = + _class.staticFieldId( + r'MEDIA_METRICS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String MEDIA_METRICS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get MEDIA_METRICS_SERVICE => _id_MEDIA_METRICS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MEDIA_PROJECTION_SERVICE = + _class.staticFieldId( + r'MEDIA_PROJECTION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String MEDIA_PROJECTION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get MEDIA_PROJECTION_SERVICE => _id_MEDIA_PROJECTION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MEDIA_QUALITY_SERVICE = + _class.staticFieldId( + r'MEDIA_QUALITY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String MEDIA_QUALITY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get MEDIA_QUALITY_SERVICE => _id_MEDIA_QUALITY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MEDIA_ROUTER_SERVICE = + _class.staticFieldId( + r'MEDIA_ROUTER_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String MEDIA_ROUTER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get MEDIA_ROUTER_SERVICE => _id_MEDIA_ROUTER_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MEDIA_SESSION_SERVICE = + _class.staticFieldId( + r'MEDIA_SESSION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String MEDIA_SESSION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get MEDIA_SESSION_SERVICE => _id_MEDIA_SESSION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MIDI_SERVICE = + _class.staticFieldId( + r'MIDI_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String MIDI_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get MIDI_SERVICE => _id_MIDI_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int MODE_APPEND` + static const MODE_APPEND = 32768; + /// from: `static public final int MODE_ENABLE_WRITE_AHEAD_LOGGING` + static const MODE_ENABLE_WRITE_AHEAD_LOGGING = 8; + /// from: `static public final int MODE_MULTI_PROCESS` + static const MODE_MULTI_PROCESS = 4; + /// from: `static public final int MODE_NO_LOCALIZED_COLLATORS` + static const MODE_NO_LOCALIZED_COLLATORS = 16; + /// from: `static public final int MODE_PRIVATE` + static const MODE_PRIVATE = 0; + /// from: `static public final int MODE_WORLD_READABLE` + static const MODE_WORLD_READABLE = 1; + /// from: `static public final int MODE_WORLD_WRITEABLE` + static const MODE_WORLD_WRITEABLE = 2; + static final _id_NETWORK_STATS_SERVICE = + _class.staticFieldId( + r'NETWORK_STATS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String NETWORK_STATS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get NETWORK_STATS_SERVICE => _id_NETWORK_STATS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_NFC_SERVICE = + _class.staticFieldId( + r'NFC_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String NFC_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get NFC_SERVICE => _id_NFC_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_NOTIFICATION_SERVICE = + _class.staticFieldId( + r'NOTIFICATION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String NOTIFICATION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get NOTIFICATION_SERVICE => _id_NOTIFICATION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_NSD_SERVICE = + _class.staticFieldId( + r'NSD_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String NSD_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get NSD_SERVICE => _id_NSD_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_OVERLAY_SERVICE = + _class.staticFieldId( + r'OVERLAY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String OVERLAY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get OVERLAY_SERVICE => _id_OVERLAY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PEOPLE_SERVICE = + _class.staticFieldId( + r'PEOPLE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String PEOPLE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get PEOPLE_SERVICE => _id_PEOPLE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PERFORMANCE_HINT_SERVICE = + _class.staticFieldId( + r'PERFORMANCE_HINT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String PERFORMANCE_HINT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get PERFORMANCE_HINT_SERVICE => _id_PERFORMANCE_HINT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PERSISTENT_DATA_BLOCK_SERVICE = + _class.staticFieldId( + r'PERSISTENT_DATA_BLOCK_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String PERSISTENT_DATA_BLOCK_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get PERSISTENT_DATA_BLOCK_SERVICE => _id_PERSISTENT_DATA_BLOCK_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_POWER_SERVICE = + _class.staticFieldId( + r'POWER_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String POWER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get POWER_SERVICE => _id_POWER_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PRINT_SERVICE = + _class.staticFieldId( + r'PRINT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String PRINT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get PRINT_SERVICE => _id_PRINT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PROFILING_SERVICE = + _class.staticFieldId( + r'PROFILING_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String PROFILING_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get PROFILING_SERVICE => _id_PROFILING_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int RECEIVER_EXPORTED` + static const RECEIVER_EXPORTED = 2; + /// from: `static public final int RECEIVER_NOT_EXPORTED` + static const RECEIVER_NOT_EXPORTED = 4; + /// from: `static public final int RECEIVER_VISIBLE_TO_INSTANT_APPS` + static const RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; + static final _id_RESTRICTIONS_SERVICE = + _class.staticFieldId( + r'RESTRICTIONS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String RESTRICTIONS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get RESTRICTIONS_SERVICE => _id_RESTRICTIONS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ROLE_SERVICE = + _class.staticFieldId( + r'ROLE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String ROLE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get ROLE_SERVICE => _id_ROLE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SATELLITE_SERVICE = + _class.staticFieldId( + r'SATELLITE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String SATELLITE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get SATELLITE_SERVICE => _id_SATELLITE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SEARCH_SERVICE = + _class.staticFieldId( + r'SEARCH_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String SEARCH_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get SEARCH_SERVICE => _id_SEARCH_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SECURITY_STATE_SERVICE = + _class.staticFieldId( + r'SECURITY_STATE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String SECURITY_STATE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get SECURITY_STATE_SERVICE => _id_SECURITY_STATE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SENSOR_SERVICE = + _class.staticFieldId( + r'SENSOR_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String SENSOR_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get SENSOR_SERVICE => _id_SENSOR_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SHORTCUT_SERVICE = + _class.staticFieldId( + r'SHORTCUT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String SHORTCUT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get SHORTCUT_SERVICE => _id_SHORTCUT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_STATUS_BAR_SERVICE = + _class.staticFieldId( + r'STATUS_BAR_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String STATUS_BAR_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get STATUS_BAR_SERVICE => _id_STATUS_BAR_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_STORAGE_SERVICE = + _class.staticFieldId( + r'STORAGE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String STORAGE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get STORAGE_SERVICE => _id_STORAGE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_STORAGE_STATS_SERVICE = + _class.staticFieldId( + r'STORAGE_STATS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String STORAGE_STATS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get STORAGE_STATS_SERVICE => _id_STORAGE_STATS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SYSTEM_HEALTH_SERVICE = + _class.staticFieldId( + r'SYSTEM_HEALTH_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String SYSTEM_HEALTH_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get SYSTEM_HEALTH_SERVICE => _id_SYSTEM_HEALTH_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TELECOM_SERVICE = + _class.staticFieldId( + r'TELECOM_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TELECOM_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TELECOM_SERVICE => _id_TELECOM_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TELEPHONY_IMS_SERVICE = + _class.staticFieldId( + r'TELEPHONY_IMS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TELEPHONY_IMS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TELEPHONY_IMS_SERVICE => _id_TELEPHONY_IMS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TELEPHONY_SERVICE = + _class.staticFieldId( + r'TELEPHONY_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TELEPHONY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TELEPHONY_SERVICE => _id_TELEPHONY_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TELEPHONY_SUBSCRIPTION_SERVICE = + _class.staticFieldId( + r'TELEPHONY_SUBSCRIPTION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TELEPHONY_SUBSCRIPTION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TELEPHONY_SUBSCRIPTION_SERVICE => _id_TELEPHONY_SUBSCRIPTION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TETHERING_SERVICE = + _class.staticFieldId( + r'TETHERING_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TETHERING_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TETHERING_SERVICE => _id_TETHERING_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TEXT_CLASSIFICATION_SERVICE = + _class.staticFieldId( + r'TEXT_CLASSIFICATION_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TEXT_CLASSIFICATION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TEXT_CLASSIFICATION_SERVICE => _id_TEXT_CLASSIFICATION_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TEXT_SERVICES_MANAGER_SERVICE = + _class.staticFieldId( + r'TEXT_SERVICES_MANAGER_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TEXT_SERVICES_MANAGER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TEXT_SERVICES_MANAGER_SERVICE => _id_TEXT_SERVICES_MANAGER_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TV_AD_SERVICE = + _class.staticFieldId( + r'TV_AD_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TV_AD_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TV_AD_SERVICE => _id_TV_AD_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TV_INPUT_SERVICE = + _class.staticFieldId( + r'TV_INPUT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TV_INPUT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TV_INPUT_SERVICE => _id_TV_INPUT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TV_INTERACTIVE_APP_SERVICE = + _class.staticFieldId( + r'TV_INTERACTIVE_APP_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String TV_INTERACTIVE_APP_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get TV_INTERACTIVE_APP_SERVICE => _id_TV_INTERACTIVE_APP_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_UI_MODE_SERVICE = + _class.staticFieldId( + r'UI_MODE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String UI_MODE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get UI_MODE_SERVICE => _id_UI_MODE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_USAGE_STATS_SERVICE = + _class.staticFieldId( + r'USAGE_STATS_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String USAGE_STATS_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get USAGE_STATS_SERVICE => _id_USAGE_STATS_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_USB_SERVICE = + _class.staticFieldId( + r'USB_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String USB_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get USB_SERVICE => _id_USB_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_USER_SERVICE = + _class.staticFieldId( + r'USER_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String USER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get USER_SERVICE => _id_USER_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_VIBRATOR_MANAGER_SERVICE = + _class.staticFieldId( + r'VIBRATOR_MANAGER_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String VIBRATOR_MANAGER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get VIBRATOR_MANAGER_SERVICE => _id_VIBRATOR_MANAGER_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_VIBRATOR_SERVICE = + _class.staticFieldId( + r'VIBRATOR_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String VIBRATOR_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get VIBRATOR_SERVICE => _id_VIBRATOR_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_VIRTUAL_DEVICE_SERVICE = + _class.staticFieldId( + r'VIRTUAL_DEVICE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String VIRTUAL_DEVICE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get VIRTUAL_DEVICE_SERVICE => _id_VIRTUAL_DEVICE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_VPN_MANAGEMENT_SERVICE = + _class.staticFieldId( + r'VPN_MANAGEMENT_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String VPN_MANAGEMENT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get VPN_MANAGEMENT_SERVICE => _id_VPN_MANAGEMENT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WALLPAPER_SERVICE = + _class.staticFieldId( + r'WALLPAPER_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String WALLPAPER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get WALLPAPER_SERVICE => _id_WALLPAPER_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WIFI_AWARE_SERVICE = + _class.staticFieldId( + r'WIFI_AWARE_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String WIFI_AWARE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get WIFI_AWARE_SERVICE => _id_WIFI_AWARE_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WIFI_P2P_SERVICE = + _class.staticFieldId( + r'WIFI_P2P_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String WIFI_P2P_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get WIFI_P2P_SERVICE => _id_WIFI_P2P_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WIFI_RTT_RANGING_SERVICE = + _class.staticFieldId( + r'WIFI_RTT_RANGING_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String WIFI_RTT_RANGING_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get WIFI_RTT_RANGING_SERVICE => _id_WIFI_RTT_RANGING_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WIFI_SERVICE = + _class.staticFieldId( + r'WIFI_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String WIFI_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get WIFI_SERVICE => _id_WIFI_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WINDOW_SERVICE = + _class.staticFieldId( + r'WINDOW_SERVICE', + r'Ljava/lang/String;', + ); + /// from: `static public final java.lang.String WINDOW_SERVICE` + /// The returned object must be released after use, by calling the [release] method. +static jni$_.JString? get WINDOW_SERVICE => _id_WINDOW_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_bindIsolatedService = _class.instanceMethodId( + r'bindIsolatedService', + r'(Landroid/content/Intent;Landroid/content/Context$BindServiceFlags;Ljava/lang/String;Ljava/util/concurrent/Executor;Landroid/content/ServiceConnection;)Z', + ); + + static final _bindIsolatedService = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public boolean bindIsolatedService(android.content.Intent intent, android.content.Context$BindServiceFlags bindServiceFlags, java.lang.String string, java.util.concurrent.Executor executor, android.content.ServiceConnection serviceConnection)` + bool bindIsolatedService(Intent? intent, Context$BindServiceFlags? bindServiceFlags, jni$_.JString? string, jni$_.JObject? executor, jni$_.JObject? serviceConnection, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bindServiceFlags = bindServiceFlags?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$executor = executor?.reference ?? jni$_.jNullReference; + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + return _bindIsolatedService(reference.pointer, _id_bindIsolatedService as jni$_.JMethodIDPtr, _$intent.pointer, _$bindServiceFlags.pointer, _$string.pointer, _$executor.pointer, _$serviceConnection.pointer).boolean; + } + + static final _id_bindIsolatedService$1 = _class.instanceMethodId( + r'bindIsolatedService', + r'(Landroid/content/Intent;ILjava/lang/String;Ljava/util/concurrent/Executor;Landroid/content/ServiceConnection;)Z', + ); + + static final _bindIsolatedService$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public boolean bindIsolatedService(android.content.Intent intent, int i, java.lang.String string, java.util.concurrent.Executor executor, android.content.ServiceConnection serviceConnection)` + bool bindIsolatedService$1(Intent? intent, int i, jni$_.JString? string, jni$_.JObject? executor, jni$_.JObject? serviceConnection, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$executor = executor?.reference ?? jni$_.jNullReference; + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + return _bindIsolatedService$1(reference.pointer, _id_bindIsolatedService$1 as jni$_.JMethodIDPtr, _$intent.pointer, i, _$string.pointer, _$executor.pointer, _$serviceConnection.pointer).boolean; + } + + static final _id_bindService = _class.instanceMethodId( + r'bindService', + r'(Landroid/content/Intent;Landroid/content/Context$BindServiceFlags;Ljava/util/concurrent/Executor;Landroid/content/ServiceConnection;)Z', + ); + + static final _bindService = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public boolean bindService(android.content.Intent intent, android.content.Context$BindServiceFlags bindServiceFlags, java.util.concurrent.Executor executor, android.content.ServiceConnection serviceConnection)` + bool bindService(Intent? intent, Context$BindServiceFlags? bindServiceFlags, jni$_.JObject? executor, jni$_.JObject? serviceConnection, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bindServiceFlags = bindServiceFlags?.reference ?? jni$_.jNullReference; + final _$executor = executor?.reference ?? jni$_.jNullReference; + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + return _bindService(reference.pointer, _id_bindService as jni$_.JMethodIDPtr, _$intent.pointer, _$bindServiceFlags.pointer, _$executor.pointer, _$serviceConnection.pointer).boolean; + } + + static final _id_bindService$1 = _class.instanceMethodId( + r'bindService', + r'(Landroid/content/Intent;Landroid/content/ServiceConnection;Landroid/content/Context$BindServiceFlags;)Z', + ); + + static final _bindService$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public boolean bindService(android.content.Intent intent, android.content.ServiceConnection serviceConnection, android.content.Context$BindServiceFlags bindServiceFlags)` + bool bindService$1(Intent? intent, jni$_.JObject? serviceConnection, Context$BindServiceFlags? bindServiceFlags, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + final _$bindServiceFlags = bindServiceFlags?.reference ?? jni$_.jNullReference; + return _bindService$1(reference.pointer, _id_bindService$1 as jni$_.JMethodIDPtr, _$intent.pointer, _$serviceConnection.pointer, _$bindServiceFlags.pointer).boolean; + } + + static final _id_bindService$2 = _class.instanceMethodId( + r'bindService', + r'(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z', + ); + + static final _bindService$2 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, int)>(); + /// from: `public abstract boolean bindService(android.content.Intent intent, android.content.ServiceConnection serviceConnection, int i)` + bool bindService$2(Intent? intent, jni$_.JObject? serviceConnection, int i, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + return _bindService$2(reference.pointer, _id_bindService$2 as jni$_.JMethodIDPtr, _$intent.pointer, _$serviceConnection.pointer, i).boolean; + } + + static final _id_bindService$3 = _class.instanceMethodId( + r'bindService', + r'(Landroid/content/Intent;ILjava/util/concurrent/Executor;Landroid/content/ServiceConnection;)Z', + ); + + static final _bindService$3 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public boolean bindService(android.content.Intent intent, int i, java.util.concurrent.Executor executor, android.content.ServiceConnection serviceConnection)` + bool bindService$3(Intent? intent, int i, jni$_.JObject? executor, jni$_.JObject? serviceConnection, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$executor = executor?.reference ?? jni$_.jNullReference; + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + return _bindService$3(reference.pointer, _id_bindService$3 as jni$_.JMethodIDPtr, _$intent.pointer, i, _$executor.pointer, _$serviceConnection.pointer).boolean; + } + + static final _id_bindServiceAsUser = _class.instanceMethodId( + r'bindServiceAsUser', + r'(Landroid/content/Intent;Landroid/content/ServiceConnection;Landroid/content/Context$BindServiceFlags;Landroid/os/UserHandle;)Z', + ); + + static final _bindServiceAsUser = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public boolean bindServiceAsUser(android.content.Intent intent, android.content.ServiceConnection serviceConnection, android.content.Context$BindServiceFlags bindServiceFlags, android.os.UserHandle userHandle)` + bool bindServiceAsUser(Intent? intent, jni$_.JObject? serviceConnection, Context$BindServiceFlags? bindServiceFlags, jni$_.JObject? userHandle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + final _$bindServiceFlags = bindServiceFlags?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + return _bindServiceAsUser(reference.pointer, _id_bindServiceAsUser as jni$_.JMethodIDPtr, _$intent.pointer, _$serviceConnection.pointer, _$bindServiceFlags.pointer, _$userHandle.pointer).boolean; + } + + static final _id_bindServiceAsUser$1 = _class.instanceMethodId( + r'bindServiceAsUser', + r'(Landroid/content/Intent;Landroid/content/ServiceConnection;ILandroid/os/UserHandle;)Z', + ); + + static final _bindServiceAsUser$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, int, jni$_.Pointer)>(); + /// from: `public boolean bindServiceAsUser(android.content.Intent intent, android.content.ServiceConnection serviceConnection, int i, android.os.UserHandle userHandle)` + bool bindServiceAsUser$1(Intent? intent, jni$_.JObject? serviceConnection, int i, jni$_.JObject? userHandle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + return _bindServiceAsUser$1(reference.pointer, _id_bindServiceAsUser$1 as jni$_.JMethodIDPtr, _$intent.pointer, _$serviceConnection.pointer, i, _$userHandle.pointer).boolean; + } + + static final _id_checkCallingOrSelfPermission = _class.instanceMethodId( + r'checkCallingOrSelfPermission', + r'(Ljava/lang/String;)I', + ); + + static final _checkCallingOrSelfPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract int checkCallingOrSelfPermission(java.lang.String string)` + int checkCallingOrSelfPermission(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _checkCallingOrSelfPermission(reference.pointer, _id_checkCallingOrSelfPermission as jni$_.JMethodIDPtr, _$string.pointer).integer; + } + + static final _id_checkCallingOrSelfUriPermission = _class.instanceMethodId( + r'checkCallingOrSelfUriPermission', + r'(Landroid/net/Uri;I)I', + ); + + static final _checkCallingOrSelfUriPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public abstract int checkCallingOrSelfUriPermission(android.net.Uri uri, int i)` + int checkCallingOrSelfUriPermission(jni$_.JObject? uri, int i, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + return _checkCallingOrSelfUriPermission(reference.pointer, _id_checkCallingOrSelfUriPermission as jni$_.JMethodIDPtr, _$uri.pointer, i).integer; + } + + static final _id_checkCallingOrSelfUriPermissions = _class.instanceMethodId( + r'checkCallingOrSelfUriPermissions', + r'(Ljava/util/List;I)[I', + ); + + static final _checkCallingOrSelfUriPermissions = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public int[] checkCallingOrSelfUriPermissions(java.util.List list, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JIntArray? checkCallingOrSelfUriPermissions(jni$_.JList? list, int i, ){ + + final _$list = list?.reference ?? jni$_.jNullReference; + return _checkCallingOrSelfUriPermissions(reference.pointer, _id_checkCallingOrSelfUriPermissions as jni$_.JMethodIDPtr, _$list.pointer, i).object(const jni$_.$JIntArray$NullableType$()); + } + + static final _id_checkCallingPermission = _class.instanceMethodId( + r'checkCallingPermission', + r'(Ljava/lang/String;)I', + ); + + static final _checkCallingPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract int checkCallingPermission(java.lang.String string)` + int checkCallingPermission(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _checkCallingPermission(reference.pointer, _id_checkCallingPermission as jni$_.JMethodIDPtr, _$string.pointer).integer; + } + + static final _id_checkCallingUriPermission = _class.instanceMethodId( + r'checkCallingUriPermission', + r'(Landroid/net/Uri;I)I', + ); + + static final _checkCallingUriPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public abstract int checkCallingUriPermission(android.net.Uri uri, int i)` + int checkCallingUriPermission(jni$_.JObject? uri, int i, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + return _checkCallingUriPermission(reference.pointer, _id_checkCallingUriPermission as jni$_.JMethodIDPtr, _$uri.pointer, i).integer; + } + + static final _id_checkCallingUriPermissions = _class.instanceMethodId( + r'checkCallingUriPermissions', + r'(Ljava/util/List;I)[I', + ); + + static final _checkCallingUriPermissions = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public int[] checkCallingUriPermissions(java.util.List list, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JIntArray? checkCallingUriPermissions(jni$_.JList? list, int i, ){ + + final _$list = list?.reference ?? jni$_.jNullReference; + return _checkCallingUriPermissions(reference.pointer, _id_checkCallingUriPermissions as jni$_.JMethodIDPtr, _$list.pointer, i).object(const jni$_.$JIntArray$NullableType$()); + } + + static final _id_checkContentUriPermissionFull = _class.instanceMethodId( + r'checkContentUriPermissionFull', + r'(Landroid/net/Uri;III)I', + ); + + static final _checkContentUriPermissionFull = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, int, int)>(); + /// from: `public int checkContentUriPermissionFull(android.net.Uri uri, int i, int i1, int i2)` + int checkContentUriPermissionFull(jni$_.JObject? uri, int i, int i1, int i2, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + return _checkContentUriPermissionFull(reference.pointer, _id_checkContentUriPermissionFull as jni$_.JMethodIDPtr, _$uri.pointer, i, i1, i2).integer; + } + + static final _id_checkPermission = _class.instanceMethodId( + r'checkPermission', + r'(Ljava/lang/String;II)I', + ); + + static final _checkPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, int)>(); + /// from: `public abstract int checkPermission(java.lang.String string, int i, int i1)` + int checkPermission(jni$_.JString? string, int i, int i1, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _checkPermission(reference.pointer, _id_checkPermission as jni$_.JMethodIDPtr, _$string.pointer, i, i1).integer; + } + + static final _id_checkSelfPermission = _class.instanceMethodId( + r'checkSelfPermission', + r'(Ljava/lang/String;)I', + ); + + static final _checkSelfPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract int checkSelfPermission(java.lang.String string)` + int checkSelfPermission(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _checkSelfPermission(reference.pointer, _id_checkSelfPermission as jni$_.JMethodIDPtr, _$string.pointer).integer; + } + + static final _id_checkUriPermission = _class.instanceMethodId( + r'checkUriPermission', + r'(Landroid/net/Uri;III)I', + ); + + static final _checkUriPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, int, int)>(); + /// from: `public abstract int checkUriPermission(android.net.Uri uri, int i, int i1, int i2)` + int checkUriPermission(jni$_.JObject? uri, int i, int i1, int i2, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + return _checkUriPermission(reference.pointer, _id_checkUriPermission as jni$_.JMethodIDPtr, _$uri.pointer, i, i1, i2).integer; + } + + static final _id_checkUriPermission$1 = _class.instanceMethodId( + r'checkUriPermission', + r'(Landroid/net/Uri;Ljava/lang/String;Ljava/lang/String;III)I', + ); + + static final _checkUriPermission$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, int, int, int)>(); + /// from: `public abstract int checkUriPermission(android.net.Uri uri, java.lang.String string, java.lang.String string1, int i, int i1, int i2)` + int checkUriPermission$1(jni$_.JObject? uri, jni$_.JString? string, jni$_.JString? string1, int i, int i1, int i2, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _checkUriPermission$1(reference.pointer, _id_checkUriPermission$1 as jni$_.JMethodIDPtr, _$uri.pointer, _$string.pointer, _$string1.pointer, i, i1, i2).integer; + } + + static final _id_checkUriPermissions = _class.instanceMethodId( + r'checkUriPermissions', + r'(Ljava/util/List;III)[I', + ); + + static final _checkUriPermissions = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, int, int)>(); + /// from: `public int[] checkUriPermissions(java.util.List list, int i, int i1, int i2)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JIntArray? checkUriPermissions(jni$_.JList? list, int i, int i1, int i2, ){ + + final _$list = list?.reference ?? jni$_.jNullReference; + return _checkUriPermissions(reference.pointer, _id_checkUriPermissions as jni$_.JMethodIDPtr, _$list.pointer, i, i1, i2).object(const jni$_.$JIntArray$NullableType$()); + } + + static final _id_clearWallpaper = _class.instanceMethodId( + r'clearWallpaper', + r'()V', + ); + + static final _clearWallpaper = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract void clearWallpaper()` + void clearWallpaper(){ + + + _clearWallpaper(reference.pointer, _id_clearWallpaper as jni$_.JMethodIDPtr).check(); + } + + static final _id_createAttributionContext = _class.instanceMethodId( + r'createAttributionContext', + r'(Ljava/lang/String;)Landroid/content/Context;', + ); + + static final _createAttributionContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Context createAttributionContext(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Context? createAttributionContext(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _createAttributionContext(reference.pointer, _id_createAttributionContext as jni$_.JMethodIDPtr, _$string.pointer).object(const $Context$NullableType$()); + } + + static final _id_createConfigurationContext = _class.instanceMethodId( + r'createConfigurationContext', + r'(Landroid/content/res/Configuration;)Landroid/content/Context;', + ); + + static final _createConfigurationContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract android.content.Context createConfigurationContext(android.content.res.Configuration configuration)` + /// The returned object must be released after use, by calling the [release] method. + Context? createConfigurationContext(jni$_.JObject? configuration, ){ + + final _$configuration = configuration?.reference ?? jni$_.jNullReference; + return _createConfigurationContext(reference.pointer, _id_createConfigurationContext as jni$_.JMethodIDPtr, _$configuration.pointer).object(const $Context$NullableType$()); + } + + static final _id_createContext = _class.instanceMethodId( + r'createContext', + r'(Landroid/content/ContextParams;)Landroid/content/Context;', + ); + + static final _createContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public android.content.Context createContext(android.content.ContextParams contextParams)` + /// The returned object must be released after use, by calling the [release] method. + Context? createContext(jni$_.JObject? contextParams, ){ + + final _$contextParams = contextParams?.reference ?? jni$_.jNullReference; + return _createContext(reference.pointer, _id_createContext as jni$_.JMethodIDPtr, _$contextParams.pointer).object(const $Context$NullableType$()); + } + + static final _id_createContextForSplit = _class.instanceMethodId( + r'createContextForSplit', + r'(Ljava/lang/String;)Landroid/content/Context;', + ); + + static final _createContextForSplit = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract android.content.Context createContextForSplit(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Context? createContextForSplit(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _createContextForSplit(reference.pointer, _id_createContextForSplit as jni$_.JMethodIDPtr, _$string.pointer).object(const $Context$NullableType$()); + } + + static final _id_createDeviceContext = _class.instanceMethodId( + r'createDeviceContext', + r'(I)Landroid/content/Context;', + ); + + static final _createDeviceContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public android.content.Context createDeviceContext(int i)` + /// The returned object must be released after use, by calling the [release] method. + Context? createDeviceContext(int i, ){ + + + return _createDeviceContext(reference.pointer, _id_createDeviceContext as jni$_.JMethodIDPtr, i).object(const $Context$NullableType$()); + } + + static final _id_createDeviceProtectedStorageContext = _class.instanceMethodId( + r'createDeviceProtectedStorageContext', + r'()Landroid/content/Context;', + ); + + static final _createDeviceProtectedStorageContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.content.Context createDeviceProtectedStorageContext()` + /// The returned object must be released after use, by calling the [release] method. + Context? createDeviceProtectedStorageContext(){ + + + return _createDeviceProtectedStorageContext(reference.pointer, _id_createDeviceProtectedStorageContext as jni$_.JMethodIDPtr).object(const $Context$NullableType$()); + } + + static final _id_createDisplayContext = _class.instanceMethodId( + r'createDisplayContext', + r'(Landroid/view/Display;)Landroid/content/Context;', + ); + + static final _createDisplayContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract android.content.Context createDisplayContext(android.view.Display display)` + /// The returned object must be released after use, by calling the [release] method. + Context? createDisplayContext(jni$_.JObject? display, ){ + + final _$display = display?.reference ?? jni$_.jNullReference; + return _createDisplayContext(reference.pointer, _id_createDisplayContext as jni$_.JMethodIDPtr, _$display.pointer).object(const $Context$NullableType$()); + } + + static final _id_createPackageContext = _class.instanceMethodId( + r'createPackageContext', + r'(Ljava/lang/String;I)Landroid/content/Context;', + ); + + static final _createPackageContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public abstract android.content.Context createPackageContext(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + Context? createPackageContext(jni$_.JString? string, int i, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _createPackageContext(reference.pointer, _id_createPackageContext as jni$_.JMethodIDPtr, _$string.pointer, i).object(const $Context$NullableType$()); + } + + static final _id_createWindowContext = _class.instanceMethodId( + r'createWindowContext', + r'(Landroid/view/Display;ILandroid/os/Bundle;)Landroid/content/Context;', + ); + + static final _createWindowContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, jni$_.Pointer)>(); + /// from: `public android.content.Context createWindowContext(android.view.Display display, int i, android.os.Bundle bundle)` + /// The returned object must be released after use, by calling the [release] method. + Context? createWindowContext(jni$_.JObject? display, int i, Bundle? bundle, ){ + + final _$display = display?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _createWindowContext(reference.pointer, _id_createWindowContext as jni$_.JMethodIDPtr, _$display.pointer, i, _$bundle.pointer).object(const $Context$NullableType$()); + } + + static final _id_createWindowContext$1 = _class.instanceMethodId( + r'createWindowContext', + r'(ILandroid/os/Bundle;)Landroid/content/Context;', + ); + + static final _createWindowContext$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int, jni$_.Pointer)>(); + /// from: `public android.content.Context createWindowContext(int i, android.os.Bundle bundle)` + /// The returned object must be released after use, by calling the [release] method. + Context? createWindowContext$1(int i, Bundle? bundle, ){ + + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _createWindowContext$1(reference.pointer, _id_createWindowContext$1 as jni$_.JMethodIDPtr, i, _$bundle.pointer).object(const $Context$NullableType$()); + } + + static final _id_databaseList = _class.instanceMethodId( + r'databaseList', + r'()[Ljava/lang/String;', + ); + + static final _databaseList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.lang.String[] databaseList()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? databaseList(){ + + + return _databaseList(reference.pointer, _id_databaseList as jni$_.JMethodIDPtr).object?>(const jni$_.$JArray$NullableType$(jni$_.$JString$NullableType$())); + } + + static final _id_deleteDatabase = _class.instanceMethodId( + r'deleteDatabase', + r'(Ljava/lang/String;)Z', + ); + + static final _deleteDatabase = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract boolean deleteDatabase(java.lang.String string)` + bool deleteDatabase(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _deleteDatabase(reference.pointer, _id_deleteDatabase as jni$_.JMethodIDPtr, _$string.pointer).boolean; + } + + static final _id_deleteFile = _class.instanceMethodId( + r'deleteFile', + r'(Ljava/lang/String;)Z', + ); + + static final _deleteFile = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract boolean deleteFile(java.lang.String string)` + bool deleteFile(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _deleteFile(reference.pointer, _id_deleteFile as jni$_.JMethodIDPtr, _$string.pointer).boolean; + } + + static final _id_deleteSharedPreferences = _class.instanceMethodId( + r'deleteSharedPreferences', + r'(Ljava/lang/String;)Z', + ); + + static final _deleteSharedPreferences = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract boolean deleteSharedPreferences(java.lang.String string)` + bool deleteSharedPreferences(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _deleteSharedPreferences(reference.pointer, _id_deleteSharedPreferences as jni$_.JMethodIDPtr, _$string.pointer).boolean; + } + + static final _id_enforceCallingOrSelfPermission = _class.instanceMethodId( + r'enforceCallingOrSelfPermission', + r'(Ljava/lang/String;Ljava/lang/String;)V', + ); + + static final _enforceCallingOrSelfPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void enforceCallingOrSelfPermission(java.lang.String string, java.lang.String string1)` + void enforceCallingOrSelfPermission(jni$_.JString? string, jni$_.JString? string1, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + _enforceCallingOrSelfPermission(reference.pointer, _id_enforceCallingOrSelfPermission as jni$_.JMethodIDPtr, _$string.pointer, _$string1.pointer).check(); + } + + static final _id_enforceCallingOrSelfUriPermission = _class.instanceMethodId( + r'enforceCallingOrSelfUriPermission', + r'(Landroid/net/Uri;ILjava/lang/String;)V', + ); + + static final _enforceCallingOrSelfUriPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, jni$_.Pointer)>(); + /// from: `public abstract void enforceCallingOrSelfUriPermission(android.net.Uri uri, int i, java.lang.String string)` + void enforceCallingOrSelfUriPermission(jni$_.JObject? uri, int i, jni$_.JString? string, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + _enforceCallingOrSelfUriPermission(reference.pointer, _id_enforceCallingOrSelfUriPermission as jni$_.JMethodIDPtr, _$uri.pointer, i, _$string.pointer).check(); + } + + static final _id_enforceCallingPermission = _class.instanceMethodId( + r'enforceCallingPermission', + r'(Ljava/lang/String;Ljava/lang/String;)V', + ); + + static final _enforceCallingPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void enforceCallingPermission(java.lang.String string, java.lang.String string1)` + void enforceCallingPermission(jni$_.JString? string, jni$_.JString? string1, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + _enforceCallingPermission(reference.pointer, _id_enforceCallingPermission as jni$_.JMethodIDPtr, _$string.pointer, _$string1.pointer).check(); + } + + static final _id_enforceCallingUriPermission = _class.instanceMethodId( + r'enforceCallingUriPermission', + r'(Landroid/net/Uri;ILjava/lang/String;)V', + ); + + static final _enforceCallingUriPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, jni$_.Pointer)>(); + /// from: `public abstract void enforceCallingUriPermission(android.net.Uri uri, int i, java.lang.String string)` + void enforceCallingUriPermission(jni$_.JObject? uri, int i, jni$_.JString? string, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + _enforceCallingUriPermission(reference.pointer, _id_enforceCallingUriPermission as jni$_.JMethodIDPtr, _$uri.pointer, i, _$string.pointer).check(); + } + + static final _id_enforcePermission = _class.instanceMethodId( + r'enforcePermission', + r'(Ljava/lang/String;IILjava/lang/String;)V', + ); + + static final _enforcePermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, int, jni$_.Pointer)>(); + /// from: `public abstract void enforcePermission(java.lang.String string, int i, int i1, java.lang.String string1)` + void enforcePermission(jni$_.JString? string, int i, int i1, jni$_.JString? string1, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + _enforcePermission(reference.pointer, _id_enforcePermission as jni$_.JMethodIDPtr, _$string.pointer, i, i1, _$string1.pointer).check(); + } + + static final _id_enforceUriPermission = _class.instanceMethodId( + r'enforceUriPermission', + r'(Landroid/net/Uri;IIILjava/lang/String;)V', + ); + + static final _enforceUriPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Int32, jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, int, int, jni$_.Pointer)>(); + /// from: `public abstract void enforceUriPermission(android.net.Uri uri, int i, int i1, int i2, java.lang.String string)` + void enforceUriPermission(jni$_.JObject? uri, int i, int i1, int i2, jni$_.JString? string, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + _enforceUriPermission(reference.pointer, _id_enforceUriPermission as jni$_.JMethodIDPtr, _$uri.pointer, i, i1, i2, _$string.pointer).check(); + } + + static final _id_enforceUriPermission$1 = _class.instanceMethodId( + r'enforceUriPermission', + r'(Landroid/net/Uri;Ljava/lang/String;Ljava/lang/String;IIILjava/lang/String;)V', + ); + + static final _enforceUriPermission$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Int32, jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, int, int, int, jni$_.Pointer)>(); + /// from: `public abstract void enforceUriPermission(android.net.Uri uri, java.lang.String string, java.lang.String string1, int i, int i1, int i2, java.lang.String string2)` + void enforceUriPermission$1(jni$_.JObject? uri, jni$_.JString? string, jni$_.JString? string1, int i, int i1, int i2, jni$_.JString? string2, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + final _$string2 = string2?.reference ?? jni$_.jNullReference; + _enforceUriPermission$1(reference.pointer, _id_enforceUriPermission$1 as jni$_.JMethodIDPtr, _$uri.pointer, _$string.pointer, _$string1.pointer, i, i1, i2, _$string2.pointer).check(); + } + + static final _id_fileList = _class.instanceMethodId( + r'fileList', + r'()[Ljava/lang/String;', + ); + + static final _fileList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.lang.String[] fileList()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? fileList(){ + + + return _fileList(reference.pointer, _id_fileList as jni$_.JMethodIDPtr).object?>(const jni$_.$JArray$NullableType$(jni$_.$JString$NullableType$())); + } + + static final _id_getApplicationContext = _class.instanceMethodId( + r'getApplicationContext', + r'()Landroid/content/Context;', + ); + + static final _getApplicationContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.content.Context getApplicationContext()` + /// The returned object must be released after use, by calling the [release] method. + Context? getApplicationContext(){ + + + return _getApplicationContext(reference.pointer, _id_getApplicationContext as jni$_.JMethodIDPtr).object(const $Context$NullableType$()); + } + + static final _id_getApplicationInfo = _class.instanceMethodId( + r'getApplicationInfo', + r'()Landroid/content/pm/ApplicationInfo;', + ); + + static final _getApplicationInfo = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.content.pm.ApplicationInfo getApplicationInfo()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationInfo(){ + + + return _getApplicationInfo(reference.pointer, _id_getApplicationInfo as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getAssets = _class.instanceMethodId( + r'getAssets', + r'()Landroid/content/res/AssetManager;', + ); + + static final _getAssets = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.content.res.AssetManager getAssets()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getAssets(){ + + + return _getAssets(reference.pointer, _id_getAssets as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getAttributionSource = _class.instanceMethodId( + r'getAttributionSource', + r'()Landroid/content/AttributionSource;', + ); + + static final _getAttributionSource = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.content.AttributionSource getAttributionSource()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getAttributionSource(){ + + + return _getAttributionSource(reference.pointer, _id_getAttributionSource as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getAttributionTag = _class.instanceMethodId( + r'getAttributionTag', + r'()Ljava/lang/String;', + ); + + static final _getAttributionTag = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String getAttributionTag()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getAttributionTag(){ + + + return _getAttributionTag(reference.pointer, _id_getAttributionTag as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getCacheDir = _class.instanceMethodId( + r'getCacheDir', + r'()Ljava/io/File;', + ); + + static final _getCacheDir = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File getCacheDir()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCacheDir(){ + + + return _getCacheDir(reference.pointer, _id_getCacheDir as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getClassLoader = _class.instanceMethodId( + r'getClassLoader', + r'()Ljava/lang/ClassLoader;', + ); + + static final _getClassLoader = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.lang.ClassLoader getClassLoader()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getClassLoader(){ + + + return _getClassLoader(reference.pointer, _id_getClassLoader as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getCodeCacheDir = _class.instanceMethodId( + r'getCodeCacheDir', + r'()Ljava/io/File;', + ); + + static final _getCodeCacheDir = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File getCodeCacheDir()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCodeCacheDir(){ + + + return _getCodeCacheDir(reference.pointer, _id_getCodeCacheDir as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getColor = _class.instanceMethodId( + r'getColor', + r'(I)I', + ); + + static final _getColor = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public final int getColor(int i)` + int getColor(int i, ){ + + + return _getColor(reference.pointer, _id_getColor as jni$_.JMethodIDPtr, i).integer; + } + + static final _id_getColorStateList = _class.instanceMethodId( + r'getColorStateList', + r'(I)Landroid/content/res/ColorStateList;', + ); + + static final _getColorStateList = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public final android.content.res.ColorStateList getColorStateList(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getColorStateList(int i, ){ + + + return _getColorStateList(reference.pointer, _id_getColorStateList as jni$_.JMethodIDPtr, i).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getContentResolver = _class.instanceMethodId( + r'getContentResolver', + r'()Landroid/content/ContentResolver;', + ); + + static final _getContentResolver = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.content.ContentResolver getContentResolver()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getContentResolver(){ + + + return _getContentResolver(reference.pointer, _id_getContentResolver as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getDataDir = _class.instanceMethodId( + r'getDataDir', + r'()Ljava/io/File;', + ); + + static final _getDataDir = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File getDataDir()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getDataDir(){ + + + return _getDataDir(reference.pointer, _id_getDataDir as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getDatabasePath = _class.instanceMethodId( + r'getDatabasePath', + r'(Ljava/lang/String;)Ljava/io/File;', + ); + + static final _getDatabasePath = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract java.io.File getDatabasePath(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getDatabasePath(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getDatabasePath(reference.pointer, _id_getDatabasePath as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getDeviceId = _class.instanceMethodId( + r'getDeviceId', + r'()I', + ); + + static final _getDeviceId = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int getDeviceId()` + int getDeviceId(){ + + + return _getDeviceId(reference.pointer, _id_getDeviceId as jni$_.JMethodIDPtr).integer; + } + + static final _id_getDir = _class.instanceMethodId( + r'getDir', + r'(Ljava/lang/String;I)Ljava/io/File;', + ); + + static final _getDir = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public abstract java.io.File getDir(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getDir(jni$_.JString? string, int i, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getDir(reference.pointer, _id_getDir as jni$_.JMethodIDPtr, _$string.pointer, i).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getDisplay = _class.instanceMethodId( + r'getDisplay', + r'()Landroid/view/Display;', + ); + + static final _getDisplay = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.view.Display getDisplay()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getDisplay(){ + + + return _getDisplay(reference.pointer, _id_getDisplay as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getDrawable = _class.instanceMethodId( + r'getDrawable', + r'(I)Landroid/graphics/drawable/Drawable;', + ); + + static final _getDrawable = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public final android.graphics.drawable.Drawable getDrawable(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getDrawable(int i, ){ + + + return _getDrawable(reference.pointer, _id_getDrawable as jni$_.JMethodIDPtr, i).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getExternalCacheDir = _class.instanceMethodId( + r'getExternalCacheDir', + r'()Ljava/io/File;', + ); + + static final _getExternalCacheDir = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File getExternalCacheDir()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getExternalCacheDir(){ + + + return _getExternalCacheDir(reference.pointer, _id_getExternalCacheDir as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getExternalCacheDirs = _class.instanceMethodId( + r'getExternalCacheDirs', + r'()[Ljava/io/File;', + ); + + static final _getExternalCacheDirs = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File[] getExternalCacheDirs()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getExternalCacheDirs(){ + + + return _getExternalCacheDirs(reference.pointer, _id_getExternalCacheDirs as jni$_.JMethodIDPtr).object?>(const jni$_.$JArray$NullableType$(jni$_.$JObject$NullableType$())); + } + + static final _id_getExternalFilesDir = _class.instanceMethodId( + r'getExternalFilesDir', + r'(Ljava/lang/String;)Ljava/io/File;', + ); + + static final _getExternalFilesDir = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract java.io.File getExternalFilesDir(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getExternalFilesDir(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getExternalFilesDir(reference.pointer, _id_getExternalFilesDir as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getExternalFilesDirs = _class.instanceMethodId( + r'getExternalFilesDirs', + r'(Ljava/lang/String;)[Ljava/io/File;', + ); + + static final _getExternalFilesDirs = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract java.io.File[] getExternalFilesDirs(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getExternalFilesDirs(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getExternalFilesDirs(reference.pointer, _id_getExternalFilesDirs as jni$_.JMethodIDPtr, _$string.pointer).object?>(const jni$_.$JArray$NullableType$(jni$_.$JObject$NullableType$())); + } + + static final _id_getExternalMediaDirs = _class.instanceMethodId( + r'getExternalMediaDirs', + r'()[Ljava/io/File;', + ); + + static final _getExternalMediaDirs = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File[] getExternalMediaDirs()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getExternalMediaDirs(){ + + + return _getExternalMediaDirs(reference.pointer, _id_getExternalMediaDirs as jni$_.JMethodIDPtr).object?>(const jni$_.$JArray$NullableType$(jni$_.$JObject$NullableType$())); + } + + static final _id_getFileStreamPath = _class.instanceMethodId( + r'getFileStreamPath', + r'(Ljava/lang/String;)Ljava/io/File;', + ); + + static final _getFileStreamPath = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract java.io.File getFileStreamPath(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getFileStreamPath(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getFileStreamPath(reference.pointer, _id_getFileStreamPath as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getFilesDir = _class.instanceMethodId( + r'getFilesDir', + r'()Ljava/io/File;', + ); + + static final _getFilesDir = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File getFilesDir()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getFilesDir(){ + + + return _getFilesDir(reference.pointer, _id_getFilesDir as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getMainExecutor = _class.instanceMethodId( + r'getMainExecutor', + r'()Ljava/util/concurrent/Executor;', + ); + + static final _getMainExecutor = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.util.concurrent.Executor getMainExecutor()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getMainExecutor(){ + + + return _getMainExecutor(reference.pointer, _id_getMainExecutor as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getMainLooper = _class.instanceMethodId( + r'getMainLooper', + r'()Landroid/os/Looper;', + ); + + static final _getMainLooper = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.os.Looper getMainLooper()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getMainLooper(){ + + + return _getMainLooper(reference.pointer, _id_getMainLooper as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getNoBackupFilesDir = _class.instanceMethodId( + r'getNoBackupFilesDir', + r'()Ljava/io/File;', + ); + + static final _getNoBackupFilesDir = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File getNoBackupFilesDir()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getNoBackupFilesDir(){ + + + return _getNoBackupFilesDir(reference.pointer, _id_getNoBackupFilesDir as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getObbDir = _class.instanceMethodId( + r'getObbDir', + r'()Ljava/io/File;', + ); + + static final _getObbDir = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File getObbDir()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getObbDir(){ + + + return _getObbDir(reference.pointer, _id_getObbDir as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getObbDirs = _class.instanceMethodId( + r'getObbDirs', + r'()[Ljava/io/File;', + ); + + static final _getObbDirs = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.io.File[] getObbDirs()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getObbDirs(){ + + + return _getObbDirs(reference.pointer, _id_getObbDirs as jni$_.JMethodIDPtr).object?>(const jni$_.$JArray$NullableType$(jni$_.$JObject$NullableType$())); + } + + static final _id_getOpPackageName = _class.instanceMethodId( + r'getOpPackageName', + r'()Ljava/lang/String;', + ); + + static final _getOpPackageName = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public java.lang.String getOpPackageName()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getOpPackageName(){ + + + return _getOpPackageName(reference.pointer, _id_getOpPackageName as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getPackageCodePath = _class.instanceMethodId( + r'getPackageCodePath', + r'()Ljava/lang/String;', + ); + + static final _getPackageCodePath = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.lang.String getPackageCodePath()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getPackageCodePath(){ + + + return _getPackageCodePath(reference.pointer, _id_getPackageCodePath as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getPackageManager = _class.instanceMethodId( + r'getPackageManager', + r'()Landroid/content/pm/PackageManager;', + ); + + static final _getPackageManager = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.content.pm.PackageManager getPackageManager()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPackageManager(){ + + + return _getPackageManager(reference.pointer, _id_getPackageManager as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPackageName = _class.instanceMethodId( + r'getPackageName', + r'()Ljava/lang/String;', + ); + + static final _getPackageName = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.lang.String getPackageName()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getPackageName(){ + + + return _getPackageName(reference.pointer, _id_getPackageName as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getPackageResourcePath = _class.instanceMethodId( + r'getPackageResourcePath', + r'()Ljava/lang/String;', + ); + + static final _getPackageResourcePath = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract java.lang.String getPackageResourcePath()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getPackageResourcePath(){ + + + return _getPackageResourcePath(reference.pointer, _id_getPackageResourcePath as jni$_.JMethodIDPtr).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getParams = _class.instanceMethodId( + r'getParams', + r'()Landroid/content/ContextParams;', + ); + + static final _getParams = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.content.ContextParams getParams()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getParams(){ + + + return _getParams(reference.pointer, _id_getParams as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getResources = _class.instanceMethodId( + r'getResources', + r'()Landroid/content/res/Resources;', + ); + + static final _getResources = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.content.res.Resources getResources()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getResources(){ + + + return _getResources(reference.pointer, _id_getResources as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSharedPreferences = _class.instanceMethodId( + r'getSharedPreferences', + r'(Ljava/lang/String;I)Landroid/content/SharedPreferences;', + ); + + static final _getSharedPreferences = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public abstract android.content.SharedPreferences getSharedPreferences(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSharedPreferences(jni$_.JString? string, int i, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getSharedPreferences(reference.pointer, _id_getSharedPreferences as jni$_.JMethodIDPtr, _$string.pointer, i).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getString = _class.instanceMethodId( + r'getString', + r'(I)Ljava/lang/String;', + ); + + static final _getString = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public final java.lang.String getString(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getString(int i, ){ + + + return _getString(reference.pointer, _id_getString as jni$_.JMethodIDPtr, i).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getString$1 = _class.instanceMethodId( + r'getString', + r'(I[Ljava/lang/Object;)Ljava/lang/String;', + ); + + static final _getString$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int, jni$_.Pointer)>(); + /// from: `public final java.lang.String getString(int i, java.lang.Object[] objects)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getString$1(int i, jni$_.JArray? objects, ){ + + final _$objects = objects?.reference ?? jni$_.jNullReference; + return _getString$1(reference.pointer, _id_getString$1 as jni$_.JMethodIDPtr, i, _$objects.pointer).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getSystemService = _class.instanceMethodId( + r'getSystemService', + r'(Ljava/lang/Class;)Ljava/lang/Object;', + ); + + static final _getSystemService = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public final T getSystemService(java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + $T? getSystemService<$T extends jni$_.JObject?>(jni$_.JObject? class$, {required jni$_.JType<$T> T, }){ + + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getSystemService(reference.pointer, _id_getSystemService as jni$_.JMethodIDPtr, _$class$.pointer).object<$T?>(T.nullableType); + } + + static final _id_getSystemService$1 = _class.instanceMethodId( + r'getSystemService', + r'(Ljava/lang/String;)Ljava/lang/Object;', + ); + + static final _getSystemService$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract java.lang.Object getSystemService(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSystemService$1(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _getSystemService$1(reference.pointer, _id_getSystemService$1 as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSystemServiceName = _class.instanceMethodId( + r'getSystemServiceName', + r'(Ljava/lang/Class;)Ljava/lang/String;', + ); + + static final _getSystemServiceName = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract java.lang.String getSystemServiceName(java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getSystemServiceName(jni$_.JObject? class$, ){ + + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getSystemServiceName(reference.pointer, _id_getSystemServiceName as jni$_.JMethodIDPtr, _$class$.pointer).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getText = _class.instanceMethodId( + r'getText', + r'(I)Ljava/lang/CharSequence;', + ); + + static final _getText = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public final java.lang.CharSequence getText(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getText(int i, ){ + + + return _getText(reference.pointer, _id_getText as jni$_.JMethodIDPtr, i).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getTheme = _class.instanceMethodId( + r'getTheme', + r'()Landroid/content/res/Resources$Theme;', + ); + + static final _getTheme = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.content.res.Resources$Theme getTheme()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getTheme(){ + + + return _getTheme(reference.pointer, _id_getTheme as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getWallpaper = _class.instanceMethodId( + r'getWallpaper', + r'()Landroid/graphics/drawable/Drawable;', + ); + + static final _getWallpaper = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.graphics.drawable.Drawable getWallpaper()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getWallpaper(){ + + + return _getWallpaper(reference.pointer, _id_getWallpaper as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getWallpaperDesiredMinimumHeight = _class.instanceMethodId( + r'getWallpaperDesiredMinimumHeight', + r'()I', + ); + + static final _getWallpaperDesiredMinimumHeight = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract int getWallpaperDesiredMinimumHeight()` + int getWallpaperDesiredMinimumHeight(){ + + + return _getWallpaperDesiredMinimumHeight(reference.pointer, _id_getWallpaperDesiredMinimumHeight as jni$_.JMethodIDPtr).integer; + } + + static final _id_getWallpaperDesiredMinimumWidth = _class.instanceMethodId( + r'getWallpaperDesiredMinimumWidth', + r'()I', + ); + + static final _getWallpaperDesiredMinimumWidth = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract int getWallpaperDesiredMinimumWidth()` + int getWallpaperDesiredMinimumWidth(){ + + + return _getWallpaperDesiredMinimumWidth(reference.pointer, _id_getWallpaperDesiredMinimumWidth as jni$_.JMethodIDPtr).integer; + } + + static final _id_grantUriPermission = _class.instanceMethodId( + r'grantUriPermission', + r'(Ljava/lang/String;Landroid/net/Uri;I)V', + ); + + static final _grantUriPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, int)>(); + /// from: `public abstract void grantUriPermission(java.lang.String string, android.net.Uri uri, int i)` + void grantUriPermission(jni$_.JString? string, jni$_.JObject? uri, int i, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$uri = uri?.reference ?? jni$_.jNullReference; + _grantUriPermission(reference.pointer, _id_grantUriPermission as jni$_.JMethodIDPtr, _$string.pointer, _$uri.pointer, i).check(); + } + + static final _id_isDeviceProtectedStorage = _class.instanceMethodId( + r'isDeviceProtectedStorage', + r'()Z', + ); + + static final _isDeviceProtectedStorage = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract boolean isDeviceProtectedStorage()` + bool isDeviceProtectedStorage(){ + + + return _isDeviceProtectedStorage(reference.pointer, _id_isDeviceProtectedStorage as jni$_.JMethodIDPtr).boolean; + } + + static final _id_isRestricted = _class.instanceMethodId( + r'isRestricted', + r'()Z', + ); + + static final _isRestricted = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public boolean isRestricted()` + bool isRestricted(){ + + + return _isRestricted(reference.pointer, _id_isRestricted as jni$_.JMethodIDPtr).boolean; + } + + static final _id_isUiContext = _class.instanceMethodId( + r'isUiContext', + r'()Z', + ); + + static final _isUiContext = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public boolean isUiContext()` + bool isUiContext(){ + + + return _isUiContext(reference.pointer, _id_isUiContext as jni$_.JMethodIDPtr).boolean; + } + + static final _id_moveDatabaseFrom = _class.instanceMethodId( + r'moveDatabaseFrom', + r'(Landroid/content/Context;Ljava/lang/String;)Z', + ); + + static final _moveDatabaseFrom = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract boolean moveDatabaseFrom(android.content.Context context, java.lang.String string)` + bool moveDatabaseFrom(Context? context, jni$_.JString? string, ){ + + final _$context = context?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _moveDatabaseFrom(reference.pointer, _id_moveDatabaseFrom as jni$_.JMethodIDPtr, _$context.pointer, _$string.pointer).boolean; + } + + static final _id_moveSharedPreferencesFrom = _class.instanceMethodId( + r'moveSharedPreferencesFrom', + r'(Landroid/content/Context;Ljava/lang/String;)Z', + ); + + static final _moveSharedPreferencesFrom = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract boolean moveSharedPreferencesFrom(android.content.Context context, java.lang.String string)` + bool moveSharedPreferencesFrom(Context? context, jni$_.JString? string, ){ + + final _$context = context?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _moveSharedPreferencesFrom(reference.pointer, _id_moveSharedPreferencesFrom as jni$_.JMethodIDPtr, _$context.pointer, _$string.pointer).boolean; + } + + static final _id_obtainStyledAttributes = _class.instanceMethodId( + r'obtainStyledAttributes', + r'(Landroid/util/AttributeSet;[I)Landroid/content/res/TypedArray;', + ); + + static final _obtainStyledAttributes = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public final android.content.res.TypedArray obtainStyledAttributes(android.util.AttributeSet attributeSet, int[] is)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? obtainStyledAttributes(jni$_.JObject? attributeSet, jni$_.JIntArray? is$, ){ + + final _$attributeSet = attributeSet?.reference ?? jni$_.jNullReference; + final _$is$ = is$?.reference ?? jni$_.jNullReference; + return _obtainStyledAttributes(reference.pointer, _id_obtainStyledAttributes as jni$_.JMethodIDPtr, _$attributeSet.pointer, _$is$.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_obtainStyledAttributes$1 = _class.instanceMethodId( + r'obtainStyledAttributes', + r'(Landroid/util/AttributeSet;[III)Landroid/content/res/TypedArray;', + ); + + static final _obtainStyledAttributes$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, int, int)>(); + /// from: `public final android.content.res.TypedArray obtainStyledAttributes(android.util.AttributeSet attributeSet, int[] is, int i, int i1)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? obtainStyledAttributes$1(jni$_.JObject? attributeSet, jni$_.JIntArray? is$, int i, int i1, ){ + + final _$attributeSet = attributeSet?.reference ?? jni$_.jNullReference; + final _$is$ = is$?.reference ?? jni$_.jNullReference; + return _obtainStyledAttributes$1(reference.pointer, _id_obtainStyledAttributes$1 as jni$_.JMethodIDPtr, _$attributeSet.pointer, _$is$.pointer, i, i1).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_obtainStyledAttributes$2 = _class.instanceMethodId( + r'obtainStyledAttributes', + r'(I[I)Landroid/content/res/TypedArray;', + ); + + static final _obtainStyledAttributes$2 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, int, jni$_.Pointer)>(); + /// from: `public final android.content.res.TypedArray obtainStyledAttributes(int i, int[] is)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? obtainStyledAttributes$2(int i, jni$_.JIntArray? is$, ){ + + final _$is$ = is$?.reference ?? jni$_.jNullReference; + return _obtainStyledAttributes$2(reference.pointer, _id_obtainStyledAttributes$2 as jni$_.JMethodIDPtr, i, _$is$.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_obtainStyledAttributes$3 = _class.instanceMethodId( + r'obtainStyledAttributes', + r'([I)Landroid/content/res/TypedArray;', + ); + + static final _obtainStyledAttributes$3 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public final android.content.res.TypedArray obtainStyledAttributes(int[] is)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? obtainStyledAttributes$3(jni$_.JIntArray? is$, ){ + + final _$is$ = is$?.reference ?? jni$_.jNullReference; + return _obtainStyledAttributes$3(reference.pointer, _id_obtainStyledAttributes$3 as jni$_.JMethodIDPtr, _$is$.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_openFileInput = _class.instanceMethodId( + r'openFileInput', + r'(Ljava/lang/String;)Ljava/io/FileInputStream;', + ); + + static final _openFileInput = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract java.io.FileInputStream openFileInput(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? openFileInput(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _openFileInput(reference.pointer, _id_openFileInput as jni$_.JMethodIDPtr, _$string.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_openFileOutput = _class.instanceMethodId( + r'openFileOutput', + r'(Ljava/lang/String;I)Ljava/io/FileOutputStream;', + ); + + static final _openFileOutput = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public abstract java.io.FileOutputStream openFileOutput(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? openFileOutput(jni$_.JString? string, int i, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + return _openFileOutput(reference.pointer, _id_openFileOutput as jni$_.JMethodIDPtr, _$string.pointer, i).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_openOrCreateDatabase = _class.instanceMethodId( + r'openOrCreateDatabase', + r'(Ljava/lang/String;ILandroid/database/sqlite/SQLiteDatabase$CursorFactory;)Landroid/database/sqlite/SQLiteDatabase;', + ); + + static final _openOrCreateDatabase = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, jni$_.Pointer)>(); + /// from: `public abstract android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.lang.String string, int i, android.database.sqlite.SQLiteDatabase$CursorFactory cursorFactory)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? openOrCreateDatabase(jni$_.JString? string, int i, jni$_.JObject? cursorFactory, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$cursorFactory = cursorFactory?.reference ?? jni$_.jNullReference; + return _openOrCreateDatabase(reference.pointer, _id_openOrCreateDatabase as jni$_.JMethodIDPtr, _$string.pointer, i, _$cursorFactory.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_openOrCreateDatabase$1 = _class.instanceMethodId( + r'openOrCreateDatabase', + r'(Ljava/lang/String;ILandroid/database/sqlite/SQLiteDatabase$CursorFactory;Landroid/database/DatabaseErrorHandler;)Landroid/database/sqlite/SQLiteDatabase;', + ); + + static final _openOrCreateDatabase$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.lang.String string, int i, android.database.sqlite.SQLiteDatabase$CursorFactory cursorFactory, android.database.DatabaseErrorHandler databaseErrorHandler)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? openOrCreateDatabase$1(jni$_.JString? string, int i, jni$_.JObject? cursorFactory, jni$_.JObject? databaseErrorHandler, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$cursorFactory = cursorFactory?.reference ?? jni$_.jNullReference; + final _$databaseErrorHandler = databaseErrorHandler?.reference ?? jni$_.jNullReference; + return _openOrCreateDatabase$1(reference.pointer, _id_openOrCreateDatabase$1 as jni$_.JMethodIDPtr, _$string.pointer, i, _$cursorFactory.pointer, _$databaseErrorHandler.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_peekWallpaper = _class.instanceMethodId( + r'peekWallpaper', + r'()Landroid/graphics/drawable/Drawable;', + ); + + static final _peekWallpaper = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public abstract android.graphics.drawable.Drawable peekWallpaper()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? peekWallpaper(){ + + + return _peekWallpaper(reference.pointer, _id_peekWallpaper as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_registerComponentCallbacks = _class.instanceMethodId( + r'registerComponentCallbacks', + r'(Landroid/content/ComponentCallbacks;)V', + ); + + static final _registerComponentCallbacks = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void registerComponentCallbacks(android.content.ComponentCallbacks componentCallbacks)` + void registerComponentCallbacks(jni$_.JObject? componentCallbacks, ){ + + final _$componentCallbacks = componentCallbacks?.reference ?? jni$_.jNullReference; + _registerComponentCallbacks(reference.pointer, _id_registerComponentCallbacks as jni$_.JMethodIDPtr, _$componentCallbacks.pointer).check(); + } + + static final _id_registerDeviceIdChangeListener = _class.instanceMethodId( + r'registerDeviceIdChangeListener', + r'(Ljava/util/concurrent/Executor;Ljava/util/function/IntConsumer;)V', + ); + + static final _registerDeviceIdChangeListener = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void registerDeviceIdChangeListener(java.util.concurrent.Executor executor, java.util.function.IntConsumer intConsumer)` + void registerDeviceIdChangeListener(jni$_.JObject? executor, jni$_.JObject? intConsumer, ){ + + final _$executor = executor?.reference ?? jni$_.jNullReference; + final _$intConsumer = intConsumer?.reference ?? jni$_.jNullReference; + _registerDeviceIdChangeListener(reference.pointer, _id_registerDeviceIdChangeListener as jni$_.JMethodIDPtr, _$executor.pointer, _$intConsumer.pointer).check(); + } + + static final _id_registerReceiver = _class.instanceMethodId( + r'registerReceiver', + r'(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;', + ); + + static final _registerReceiver = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract android.content.Intent registerReceiver(android.content.BroadcastReceiver broadcastReceiver, android.content.IntentFilter intentFilter)` + /// The returned object must be released after use, by calling the [release] method. + Intent? registerReceiver(jni$_.JObject? broadcastReceiver, jni$_.JObject? intentFilter, ){ + + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$intentFilter = intentFilter?.reference ?? jni$_.jNullReference; + return _registerReceiver(reference.pointer, _id_registerReceiver as jni$_.JMethodIDPtr, _$broadcastReceiver.pointer, _$intentFilter.pointer).object(const $Intent$NullableType$()); + } + + static final _id_registerReceiver$1 = _class.instanceMethodId( + r'registerReceiver', + r'(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;I)Landroid/content/Intent;', + ); + + static final _registerReceiver$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, int)>(); + /// from: `public abstract android.content.Intent registerReceiver(android.content.BroadcastReceiver broadcastReceiver, android.content.IntentFilter intentFilter, int i)` + /// The returned object must be released after use, by calling the [release] method. + Intent? registerReceiver$1(jni$_.JObject? broadcastReceiver, jni$_.JObject? intentFilter, int i, ){ + + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$intentFilter = intentFilter?.reference ?? jni$_.jNullReference; + return _registerReceiver$1(reference.pointer, _id_registerReceiver$1 as jni$_.JMethodIDPtr, _$broadcastReceiver.pointer, _$intentFilter.pointer, i).object(const $Intent$NullableType$()); + } + + static final _id_registerReceiver$2 = _class.instanceMethodId( + r'registerReceiver', + r'(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;Ljava/lang/String;Landroid/os/Handler;)Landroid/content/Intent;', + ); + + static final _registerReceiver$2 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract android.content.Intent registerReceiver(android.content.BroadcastReceiver broadcastReceiver, android.content.IntentFilter intentFilter, java.lang.String string, android.os.Handler handler)` + /// The returned object must be released after use, by calling the [release] method. + Intent? registerReceiver$2(jni$_.JObject? broadcastReceiver, jni$_.JObject? intentFilter, jni$_.JString? string, jni$_.JObject? handler, ){ + + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$intentFilter = intentFilter?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$handler = handler?.reference ?? jni$_.jNullReference; + return _registerReceiver$2(reference.pointer, _id_registerReceiver$2 as jni$_.JMethodIDPtr, _$broadcastReceiver.pointer, _$intentFilter.pointer, _$string.pointer, _$handler.pointer).object(const $Intent$NullableType$()); + } + + static final _id_registerReceiver$3 = _class.instanceMethodId( + r'registerReceiver', + r'(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;Ljava/lang/String;Landroid/os/Handler;I)Landroid/content/Intent;', + ); + + static final _registerReceiver$3 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, int)>(); + /// from: `public abstract android.content.Intent registerReceiver(android.content.BroadcastReceiver broadcastReceiver, android.content.IntentFilter intentFilter, java.lang.String string, android.os.Handler handler, int i)` + /// The returned object must be released after use, by calling the [release] method. + Intent? registerReceiver$3(jni$_.JObject? broadcastReceiver, jni$_.JObject? intentFilter, jni$_.JString? string, jni$_.JObject? handler, int i, ){ + + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$intentFilter = intentFilter?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$handler = handler?.reference ?? jni$_.jNullReference; + return _registerReceiver$3(reference.pointer, _id_registerReceiver$3 as jni$_.JMethodIDPtr, _$broadcastReceiver.pointer, _$intentFilter.pointer, _$string.pointer, _$handler.pointer, i).object(const $Intent$NullableType$()); + } + + static final _id_removeStickyBroadcast = _class.instanceMethodId( + r'removeStickyBroadcast', + r'(Landroid/content/Intent;)V', + ); + + static final _removeStickyBroadcast = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract void removeStickyBroadcast(android.content.Intent intent)` + void removeStickyBroadcast(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + _removeStickyBroadcast(reference.pointer, _id_removeStickyBroadcast as jni$_.JMethodIDPtr, _$intent.pointer).check(); + } + + static final _id_removeStickyBroadcastAsUser = _class.instanceMethodId( + r'removeStickyBroadcastAsUser', + r'(Landroid/content/Intent;Landroid/os/UserHandle;)V', + ); + + static final _removeStickyBroadcastAsUser = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void removeStickyBroadcastAsUser(android.content.Intent intent, android.os.UserHandle userHandle)` + void removeStickyBroadcastAsUser(Intent? intent, jni$_.JObject? userHandle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + _removeStickyBroadcastAsUser(reference.pointer, _id_removeStickyBroadcastAsUser as jni$_.JMethodIDPtr, _$intent.pointer, _$userHandle.pointer).check(); + } + + static final _id_revokeSelfPermissionOnKill = _class.instanceMethodId( + r'revokeSelfPermissionOnKill', + r'(Ljava/lang/String;)V', + ); + + static final _revokeSelfPermissionOnKill = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void revokeSelfPermissionOnKill(java.lang.String string)` + void revokeSelfPermissionOnKill(jni$_.JString? string, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + _revokeSelfPermissionOnKill(reference.pointer, _id_revokeSelfPermissionOnKill as jni$_.JMethodIDPtr, _$string.pointer).check(); + } + + static final _id_revokeSelfPermissionsOnKill = _class.instanceMethodId( + r'revokeSelfPermissionsOnKill', + r'(Ljava/util/Collection;)V', + ); + + static final _revokeSelfPermissionsOnKill = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void revokeSelfPermissionsOnKill(java.util.Collection collection)` + void revokeSelfPermissionsOnKill(jni$_.JObject? collection, ){ + + final _$collection = collection?.reference ?? jni$_.jNullReference; + _revokeSelfPermissionsOnKill(reference.pointer, _id_revokeSelfPermissionsOnKill as jni$_.JMethodIDPtr, _$collection.pointer).check(); + } + + static final _id_revokeUriPermission = _class.instanceMethodId( + r'revokeUriPermission', + r'(Landroid/net/Uri;I)V', + ); + + static final _revokeUriPermission = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + /// from: `public abstract void revokeUriPermission(android.net.Uri uri, int i)` + void revokeUriPermission(jni$_.JObject? uri, int i, ){ + + final _$uri = uri?.reference ?? jni$_.jNullReference; + _revokeUriPermission(reference.pointer, _id_revokeUriPermission as jni$_.JMethodIDPtr, _$uri.pointer, i).check(); + } + + static final _id_revokeUriPermission$1 = _class.instanceMethodId( + r'revokeUriPermission', + r'(Ljava/lang/String;Landroid/net/Uri;I)V', + ); + + static final _revokeUriPermission$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, int)>(); + /// from: `public abstract void revokeUriPermission(java.lang.String string, android.net.Uri uri, int i)` + void revokeUriPermission$1(jni$_.JString? string, jni$_.JObject? uri, int i, ){ + + final _$string = string?.reference ?? jni$_.jNullReference; + final _$uri = uri?.reference ?? jni$_.jNullReference; + _revokeUriPermission$1(reference.pointer, _id_revokeUriPermission$1 as jni$_.JMethodIDPtr, _$string.pointer, _$uri.pointer, i).check(); + } + + static final _id_sendBroadcast = _class.instanceMethodId( + r'sendBroadcast', + r'(Landroid/content/Intent;)V', + ); + + static final _sendBroadcast = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract void sendBroadcast(android.content.Intent intent)` + void sendBroadcast(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + _sendBroadcast(reference.pointer, _id_sendBroadcast as jni$_.JMethodIDPtr, _$intent.pointer).check(); + } + + static final _id_sendBroadcast$1 = _class.instanceMethodId( + r'sendBroadcast', + r'(Landroid/content/Intent;Ljava/lang/String;)V', + ); + + static final _sendBroadcast$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void sendBroadcast(android.content.Intent intent, java.lang.String string)` + void sendBroadcast$1(Intent? intent, jni$_.JString? string, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + _sendBroadcast$1(reference.pointer, _id_sendBroadcast$1 as jni$_.JMethodIDPtr, _$intent.pointer, _$string.pointer).check(); + } + + static final _id_sendBroadcast$2 = _class.instanceMethodId( + r'sendBroadcast', + r'(Landroid/content/Intent;Ljava/lang/String;Landroid/os/Bundle;)V', + ); + + static final _sendBroadcast$2 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void sendBroadcast(android.content.Intent intent, java.lang.String string, android.os.Bundle bundle)` + void sendBroadcast$2(Intent? intent, jni$_.JString? string, Bundle? bundle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _sendBroadcast$2(reference.pointer, _id_sendBroadcast$2 as jni$_.JMethodIDPtr, _$intent.pointer, _$string.pointer, _$bundle.pointer).check(); + } + + static final _id_sendBroadcastAsUser = _class.instanceMethodId( + r'sendBroadcastAsUser', + r'(Landroid/content/Intent;Landroid/os/UserHandle;)V', + ); + + static final _sendBroadcastAsUser = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void sendBroadcastAsUser(android.content.Intent intent, android.os.UserHandle userHandle)` + void sendBroadcastAsUser(Intent? intent, jni$_.JObject? userHandle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + _sendBroadcastAsUser(reference.pointer, _id_sendBroadcastAsUser as jni$_.JMethodIDPtr, _$intent.pointer, _$userHandle.pointer).check(); + } + + static final _id_sendBroadcastAsUser$1 = _class.instanceMethodId( + r'sendBroadcastAsUser', + r'(Landroid/content/Intent;Landroid/os/UserHandle;Ljava/lang/String;)V', + ); + + static final _sendBroadcastAsUser$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void sendBroadcastAsUser(android.content.Intent intent, android.os.UserHandle userHandle, java.lang.String string)` + void sendBroadcastAsUser$1(Intent? intent, jni$_.JObject? userHandle, jni$_.JString? string, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + _sendBroadcastAsUser$1(reference.pointer, _id_sendBroadcastAsUser$1 as jni$_.JMethodIDPtr, _$intent.pointer, _$userHandle.pointer, _$string.pointer).check(); + } + + static final _id_sendBroadcastWithMultiplePermissions = _class.instanceMethodId( + r'sendBroadcastWithMultiplePermissions', + r'(Landroid/content/Intent;[Ljava/lang/String;)V', + ); + + static final _sendBroadcastWithMultiplePermissions = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void sendBroadcastWithMultiplePermissions(android.content.Intent intent, java.lang.String[] strings)` + void sendBroadcastWithMultiplePermissions(Intent? intent, jni$_.JArray? strings, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$strings = strings?.reference ?? jni$_.jNullReference; + _sendBroadcastWithMultiplePermissions(reference.pointer, _id_sendBroadcastWithMultiplePermissions as jni$_.JMethodIDPtr, _$intent.pointer, _$strings.pointer).check(); + } + + static final _id_sendOrderedBroadcast = _class.instanceMethodId( + r'sendOrderedBroadcast', + r'(Landroid/content/Intent;Ljava/lang/String;)V', + ); + + static final _sendOrderedBroadcast = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void sendOrderedBroadcast(android.content.Intent intent, java.lang.String string)` + void sendOrderedBroadcast(Intent? intent, jni$_.JString? string, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + _sendOrderedBroadcast(reference.pointer, _id_sendOrderedBroadcast as jni$_.JMethodIDPtr, _$intent.pointer, _$string.pointer).check(); + } + + static final _id_sendOrderedBroadcast$1 = _class.instanceMethodId( + r'sendOrderedBroadcast', + r'(Landroid/content/Intent;Ljava/lang/String;Landroid/content/BroadcastReceiver;Landroid/os/Handler;ILjava/lang/String;Landroid/os/Bundle;)V', + ); + + static final _sendOrderedBroadcast$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, int, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void sendOrderedBroadcast(android.content.Intent intent, java.lang.String string, android.content.BroadcastReceiver broadcastReceiver, android.os.Handler handler, int i, java.lang.String string1, android.os.Bundle bundle)` + void sendOrderedBroadcast$1(Intent? intent, jni$_.JString? string, jni$_.JObject? broadcastReceiver, jni$_.JObject? handler, int i, jni$_.JString? string1, Bundle? bundle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$handler = handler?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _sendOrderedBroadcast$1(reference.pointer, _id_sendOrderedBroadcast$1 as jni$_.JMethodIDPtr, _$intent.pointer, _$string.pointer, _$broadcastReceiver.pointer, _$handler.pointer, i, _$string1.pointer, _$bundle.pointer).check(); + } + + static final _id_sendOrderedBroadcast$2 = _class.instanceMethodId( + r'sendOrderedBroadcast', + r'(Landroid/content/Intent;Ljava/lang/String;Landroid/os/Bundle;)V', + ); + + static final _sendOrderedBroadcast$2 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void sendOrderedBroadcast(android.content.Intent intent, java.lang.String string, android.os.Bundle bundle)` + void sendOrderedBroadcast$2(Intent? intent, jni$_.JString? string, Bundle? bundle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _sendOrderedBroadcast$2(reference.pointer, _id_sendOrderedBroadcast$2 as jni$_.JMethodIDPtr, _$intent.pointer, _$string.pointer, _$bundle.pointer).check(); + } + + static final _id_sendOrderedBroadcast$3 = _class.instanceMethodId( + r'sendOrderedBroadcast', + r'(Landroid/content/Intent;Ljava/lang/String;Landroid/os/Bundle;Landroid/content/BroadcastReceiver;Landroid/os/Handler;ILjava/lang/String;Landroid/os/Bundle;)V', + ); + + static final _sendOrderedBroadcast$3 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, int, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void sendOrderedBroadcast(android.content.Intent intent, java.lang.String string, android.os.Bundle bundle, android.content.BroadcastReceiver broadcastReceiver, android.os.Handler handler, int i, java.lang.String string1, android.os.Bundle bundle1)` + void sendOrderedBroadcast$3(Intent? intent, jni$_.JString? string, Bundle? bundle, jni$_.JObject? broadcastReceiver, jni$_.JObject? handler, int i, jni$_.JString? string1, Bundle? bundle1, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$handler = handler?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + final _$bundle1 = bundle1?.reference ?? jni$_.jNullReference; + _sendOrderedBroadcast$3(reference.pointer, _id_sendOrderedBroadcast$3 as jni$_.JMethodIDPtr, _$intent.pointer, _$string.pointer, _$bundle.pointer, _$broadcastReceiver.pointer, _$handler.pointer, i, _$string1.pointer, _$bundle1.pointer).check(); + } + + static final _id_sendOrderedBroadcast$4 = _class.instanceMethodId( + r'sendOrderedBroadcast', + r'(Landroid/content/Intent;Ljava/lang/String;Ljava/lang/String;Landroid/content/BroadcastReceiver;Landroid/os/Handler;ILjava/lang/String;Landroid/os/Bundle;)V', + ); + + static final _sendOrderedBroadcast$4 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, int, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void sendOrderedBroadcast(android.content.Intent intent, java.lang.String string, java.lang.String string1, android.content.BroadcastReceiver broadcastReceiver, android.os.Handler handler, int i, java.lang.String string2, android.os.Bundle bundle)` + void sendOrderedBroadcast$4(Intent? intent, jni$_.JString? string, jni$_.JString? string1, jni$_.JObject? broadcastReceiver, jni$_.JObject? handler, int i, jni$_.JString? string2, Bundle? bundle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$handler = handler?.reference ?? jni$_.jNullReference; + final _$string2 = string2?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _sendOrderedBroadcast$4(reference.pointer, _id_sendOrderedBroadcast$4 as jni$_.JMethodIDPtr, _$intent.pointer, _$string.pointer, _$string1.pointer, _$broadcastReceiver.pointer, _$handler.pointer, i, _$string2.pointer, _$bundle.pointer).check(); + } + + static final _id_sendOrderedBroadcastAsUser = _class.instanceMethodId( + r'sendOrderedBroadcastAsUser', + r'(Landroid/content/Intent;Landroid/os/UserHandle;Ljava/lang/String;Landroid/content/BroadcastReceiver;Landroid/os/Handler;ILjava/lang/String;Landroid/os/Bundle;)V', + ); + + static final _sendOrderedBroadcastAsUser = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, int, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void sendOrderedBroadcastAsUser(android.content.Intent intent, android.os.UserHandle userHandle, java.lang.String string, android.content.BroadcastReceiver broadcastReceiver, android.os.Handler handler, int i, java.lang.String string1, android.os.Bundle bundle)` + void sendOrderedBroadcastAsUser(Intent? intent, jni$_.JObject? userHandle, jni$_.JString? string, jni$_.JObject? broadcastReceiver, jni$_.JObject? handler, int i, jni$_.JString? string1, Bundle? bundle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$handler = handler?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _sendOrderedBroadcastAsUser(reference.pointer, _id_sendOrderedBroadcastAsUser as jni$_.JMethodIDPtr, _$intent.pointer, _$userHandle.pointer, _$string.pointer, _$broadcastReceiver.pointer, _$handler.pointer, i, _$string1.pointer, _$bundle.pointer).check(); + } + + static final _id_sendStickyBroadcast = _class.instanceMethodId( + r'sendStickyBroadcast', + r'(Landroid/content/Intent;)V', + ); + + static final _sendStickyBroadcast = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract void sendStickyBroadcast(android.content.Intent intent)` + void sendStickyBroadcast(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + _sendStickyBroadcast(reference.pointer, _id_sendStickyBroadcast as jni$_.JMethodIDPtr, _$intent.pointer).check(); + } + + static final _id_sendStickyBroadcast$1 = _class.instanceMethodId( + r'sendStickyBroadcast', + r'(Landroid/content/Intent;Landroid/os/Bundle;)V', + ); + + static final _sendStickyBroadcast$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public void sendStickyBroadcast(android.content.Intent intent, android.os.Bundle bundle)` + void sendStickyBroadcast$1(Intent? intent, Bundle? bundle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _sendStickyBroadcast$1(reference.pointer, _id_sendStickyBroadcast$1 as jni$_.JMethodIDPtr, _$intent.pointer, _$bundle.pointer).check(); + } + + static final _id_sendStickyBroadcastAsUser = _class.instanceMethodId( + r'sendStickyBroadcastAsUser', + r'(Landroid/content/Intent;Landroid/os/UserHandle;)V', + ); + + static final _sendStickyBroadcastAsUser = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void sendStickyBroadcastAsUser(android.content.Intent intent, android.os.UserHandle userHandle)` + void sendStickyBroadcastAsUser(Intent? intent, jni$_.JObject? userHandle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + _sendStickyBroadcastAsUser(reference.pointer, _id_sendStickyBroadcastAsUser as jni$_.JMethodIDPtr, _$intent.pointer, _$userHandle.pointer).check(); + } + + static final _id_sendStickyOrderedBroadcast = _class.instanceMethodId( + r'sendStickyOrderedBroadcast', + r'(Landroid/content/Intent;Landroid/content/BroadcastReceiver;Landroid/os/Handler;ILjava/lang/String;Landroid/os/Bundle;)V', + ); + + static final _sendStickyOrderedBroadcast = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, int, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void sendStickyOrderedBroadcast(android.content.Intent intent, android.content.BroadcastReceiver broadcastReceiver, android.os.Handler handler, int i, java.lang.String string, android.os.Bundle bundle)` + void sendStickyOrderedBroadcast(Intent? intent, jni$_.JObject? broadcastReceiver, jni$_.JObject? handler, int i, jni$_.JString? string, Bundle? bundle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$handler = handler?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _sendStickyOrderedBroadcast(reference.pointer, _id_sendStickyOrderedBroadcast as jni$_.JMethodIDPtr, _$intent.pointer, _$broadcastReceiver.pointer, _$handler.pointer, i, _$string.pointer, _$bundle.pointer).check(); + } + + static final _id_sendStickyOrderedBroadcastAsUser = _class.instanceMethodId( + r'sendStickyOrderedBroadcastAsUser', + r'(Landroid/content/Intent;Landroid/os/UserHandle;Landroid/content/BroadcastReceiver;Landroid/os/Handler;ILjava/lang/String;Landroid/os/Bundle;)V', + ); + + static final _sendStickyOrderedBroadcastAsUser = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer, int, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void sendStickyOrderedBroadcastAsUser(android.content.Intent intent, android.os.UserHandle userHandle, android.content.BroadcastReceiver broadcastReceiver, android.os.Handler handler, int i, java.lang.String string, android.os.Bundle bundle)` + void sendStickyOrderedBroadcastAsUser(Intent? intent, jni$_.JObject? userHandle, jni$_.JObject? broadcastReceiver, jni$_.JObject? handler, int i, jni$_.JString? string, Bundle? bundle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$handler = handler?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _sendStickyOrderedBroadcastAsUser(reference.pointer, _id_sendStickyOrderedBroadcastAsUser as jni$_.JMethodIDPtr, _$intent.pointer, _$userHandle.pointer, _$broadcastReceiver.pointer, _$handler.pointer, i, _$string.pointer, _$bundle.pointer).check(); + } + + static final _id_setTheme = _class.instanceMethodId( + r'setTheme', + r'(I)V', + ); + + static final _setTheme = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public abstract void setTheme(int i)` + void setTheme(int i, ){ + + + _setTheme(reference.pointer, _id_setTheme as jni$_.JMethodIDPtr, i).check(); + } + + static final _id_setWallpaper = _class.instanceMethodId( + r'setWallpaper', + r'(Landroid/graphics/Bitmap;)V', + ); + + static final _setWallpaper = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract void setWallpaper(android.graphics.Bitmap bitmap)` + void setWallpaper(jni$_.JObject? bitmap, ){ + + final _$bitmap = bitmap?.reference ?? jni$_.jNullReference; + _setWallpaper(reference.pointer, _id_setWallpaper as jni$_.JMethodIDPtr, _$bitmap.pointer).check(); + } + + static final _id_setWallpaper$1 = _class.instanceMethodId( + r'setWallpaper', + r'(Ljava/io/InputStream;)V', + ); + + static final _setWallpaper$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract void setWallpaper(java.io.InputStream inputStream)` + void setWallpaper$1(jni$_.JObject? inputStream, ){ + + final _$inputStream = inputStream?.reference ?? jni$_.jNullReference; + _setWallpaper$1(reference.pointer, _id_setWallpaper$1 as jni$_.JMethodIDPtr, _$inputStream.pointer).check(); + } + + static final _id_startActivities = _class.instanceMethodId( + r'startActivities', + r'([Landroid/content/Intent;)V', + ); + + static final _startActivities = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract void startActivities(android.content.Intent[] intents)` + void startActivities(jni$_.JArray? intents, ){ + + final _$intents = intents?.reference ?? jni$_.jNullReference; + _startActivities(reference.pointer, _id_startActivities as jni$_.JMethodIDPtr, _$intents.pointer).check(); + } + + static final _id_startActivities$1 = _class.instanceMethodId( + r'startActivities', + r'([Landroid/content/Intent;Landroid/os/Bundle;)V', + ); + + static final _startActivities$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void startActivities(android.content.Intent[] intents, android.os.Bundle bundle)` + void startActivities$1(jni$_.JArray? intents, Bundle? bundle, ){ + + final _$intents = intents?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startActivities$1(reference.pointer, _id_startActivities$1 as jni$_.JMethodIDPtr, _$intents.pointer, _$bundle.pointer).check(); + } + + static final _id_startActivity = _class.instanceMethodId( + r'startActivity', + r'(Landroid/content/Intent;)V', + ); + + static final _startActivity = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract void startActivity(android.content.Intent intent)` + void startActivity(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + _startActivity(reference.pointer, _id_startActivity as jni$_.JMethodIDPtr, _$intent.pointer).check(); + } + + static final _id_startActivity$1 = _class.instanceMethodId( + r'startActivity', + r'(Landroid/content/Intent;Landroid/os/Bundle;)V', + ); + + static final _startActivity$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract void startActivity(android.content.Intent intent, android.os.Bundle bundle)` + void startActivity$1(Intent? intent, Bundle? bundle, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startActivity$1(reference.pointer, _id_startActivity$1 as jni$_.JMethodIDPtr, _$intent.pointer, _$bundle.pointer).check(); + } + + static final _id_startForegroundService = _class.instanceMethodId( + r'startForegroundService', + r'(Landroid/content/Intent;)Landroid/content/ComponentName;', + ); + + static final _startForegroundService = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract android.content.ComponentName startForegroundService(android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? startForegroundService(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _startForegroundService(reference.pointer, _id_startForegroundService as jni$_.JMethodIDPtr, _$intent.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_startInstrumentation = _class.instanceMethodId( + r'startInstrumentation', + r'(Landroid/content/ComponentName;Ljava/lang/String;Landroid/os/Bundle;)Z', + ); + + static final _startInstrumentation = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, jni$_.Pointer)>(); + /// from: `public abstract boolean startInstrumentation(android.content.ComponentName componentName, java.lang.String string, android.os.Bundle bundle)` + bool startInstrumentation(jni$_.JObject? componentName, jni$_.JString? string, Bundle? bundle, ){ + + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _startInstrumentation(reference.pointer, _id_startInstrumentation as jni$_.JMethodIDPtr, _$componentName.pointer, _$string.pointer, _$bundle.pointer).boolean; + } + + static final _id_startIntentSender = _class.instanceMethodId( + r'startIntentSender', + r'(Landroid/content/IntentSender;Landroid/content/Intent;III)V', + ); + + static final _startIntentSender = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, int, int, int)>(); + /// from: `public abstract void startIntentSender(android.content.IntentSender intentSender, android.content.Intent intent, int i, int i1, int i2)` + void startIntentSender(jni$_.JObject? intentSender, Intent? intent, int i, int i1, int i2, ){ + + final _$intentSender = intentSender?.reference ?? jni$_.jNullReference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + _startIntentSender(reference.pointer, _id_startIntentSender as jni$_.JMethodIDPtr, _$intentSender.pointer, _$intent.pointer, i, i1, i2).check(); + } + + static final _id_startIntentSender$1 = _class.instanceMethodId( + r'startIntentSender', + r'(Landroid/content/IntentSender;Landroid/content/Intent;IIILandroid/os/Bundle;)V', + ); + + static final _startIntentSender$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Int32, jni$_.Int32, jni$_.Int32, jni$_.Pointer)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, int, int, int, jni$_.Pointer)>(); + /// from: `public abstract void startIntentSender(android.content.IntentSender intentSender, android.content.Intent intent, int i, int i1, int i2, android.os.Bundle bundle)` + void startIntentSender$1(jni$_.JObject? intentSender, Intent? intent, int i, int i1, int i2, Bundle? bundle, ){ + + final _$intentSender = intentSender?.reference ?? jni$_.jNullReference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startIntentSender$1(reference.pointer, _id_startIntentSender$1 as jni$_.JMethodIDPtr, _$intentSender.pointer, _$intent.pointer, i, i1, i2, _$bundle.pointer).check(); + } + + static final _id_startService = _class.instanceMethodId( + r'startService', + r'(Landroid/content/Intent;)Landroid/content/ComponentName;', + ); + + static final _startService = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract android.content.ComponentName startService(android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? startService(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _startService(reference.pointer, _id_startService as jni$_.JMethodIDPtr, _$intent.pointer).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_stopService = _class.instanceMethodId( + r'stopService', + r'(Landroid/content/Intent;)Z', + ); + + static final _stopService = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallBooleanMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract boolean stopService(android.content.Intent intent)` + bool stopService(Intent? intent, ){ + + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _stopService(reference.pointer, _id_stopService as jni$_.JMethodIDPtr, _$intent.pointer).boolean; + } + + static final _id_unbindService = _class.instanceMethodId( + r'unbindService', + r'(Landroid/content/ServiceConnection;)V', + ); + + static final _unbindService = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract void unbindService(android.content.ServiceConnection serviceConnection)` + void unbindService(jni$_.JObject? serviceConnection, ){ + + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + _unbindService(reference.pointer, _id_unbindService as jni$_.JMethodIDPtr, _$serviceConnection.pointer).check(); + } + + static final _id_unregisterComponentCallbacks = _class.instanceMethodId( + r'unregisterComponentCallbacks', + r'(Landroid/content/ComponentCallbacks;)V', + ); + + static final _unregisterComponentCallbacks = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void unregisterComponentCallbacks(android.content.ComponentCallbacks componentCallbacks)` + void unregisterComponentCallbacks(jni$_.JObject? componentCallbacks, ){ + + final _$componentCallbacks = componentCallbacks?.reference ?? jni$_.jNullReference; + _unregisterComponentCallbacks(reference.pointer, _id_unregisterComponentCallbacks as jni$_.JMethodIDPtr, _$componentCallbacks.pointer).check(); + } + + static final _id_unregisterDeviceIdChangeListener = _class.instanceMethodId( + r'unregisterDeviceIdChangeListener', + r'(Ljava/util/function/IntConsumer;)V', + ); + + static final _unregisterDeviceIdChangeListener = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void unregisterDeviceIdChangeListener(java.util.function.IntConsumer intConsumer)` + void unregisterDeviceIdChangeListener(jni$_.JObject? intConsumer, ){ + + final _$intConsumer = intConsumer?.reference ?? jni$_.jNullReference; + _unregisterDeviceIdChangeListener(reference.pointer, _id_unregisterDeviceIdChangeListener as jni$_.JMethodIDPtr, _$intConsumer.pointer).check(); + } + + static final _id_unregisterReceiver = _class.instanceMethodId( + r'unregisterReceiver', + r'(Landroid/content/BroadcastReceiver;)V', + ); + + static final _unregisterReceiver = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public abstract void unregisterReceiver(android.content.BroadcastReceiver broadcastReceiver)` + void unregisterReceiver(jni$_.JObject? broadcastReceiver, ){ + + final _$broadcastReceiver = broadcastReceiver?.reference ?? jni$_.jNullReference; + _unregisterReceiver(reference.pointer, _id_unregisterReceiver as jni$_.JMethodIDPtr, _$broadcastReceiver.pointer).check(); + } + + static final _id_updateServiceGroup = _class.instanceMethodId( + r'updateServiceGroup', + r'(Landroid/content/ServiceConnection;II)V', + ); + + static final _updateServiceGroup = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, int)>(); + /// from: `public void updateServiceGroup(android.content.ServiceConnection serviceConnection, int i, int i1)` + void updateServiceGroup(jni$_.JObject? serviceConnection, int i, int i1, ){ + + final _$serviceConnection = serviceConnection?.reference ?? jni$_.jNullReference; + _updateServiceGroup(reference.pointer, _id_updateServiceGroup as jni$_.JMethodIDPtr, _$serviceConnection.pointer, i, i1).check(); + } + +} +final class $Context$NullableType$ extends jni$_.JType { + + + @jni$_.internal + const $Context$NullableType$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Context;'; + + @jni$_.internal + @core$_.override + Context? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Context.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Context$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Context$NullableType$) && + other is $Context$NullableType$; + } +} + +final class $Context$Type$ extends jni$_.JType { + + + @jni$_.internal + const $Context$Type$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Context;'; + + @jni$_.internal + @core$_.override + Context fromReference(jni$_.JReference reference) => + Context.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Context$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Context$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Context$Type$) && + other is $Context$Type$; + } +} + +/// from: `com.example.android_launch_activity.SecondActivity` +class SecondActivity extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + + + @jni$_.internal + SecondActivity.fromReference( + + jni$_.JReference reference, + ) : + $type = type, + super.fromReference( + + reference + ); + + static final _class = jni$_.JClass.forName(r'com/example/android_launch_activity/SecondActivity'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $SecondActivity$NullableType$(); + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $SecondActivity$Type$(); + static final _id_new$ = _class.constructorId( + r'()V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory SecondActivity() { + + + return SecondActivity.fromReference( + + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference + ); + } + +} +final class $SecondActivity$NullableType$ extends jni$_.JType { + + + @jni$_.internal + const $SecondActivity$NullableType$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/example/android_launch_activity/SecondActivity;'; + + @jni$_.internal + @core$_.override + SecondActivity? fromReference(jni$_.JReference reference) => + reference.isNull ? null : SecondActivity.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SecondActivity$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SecondActivity$NullableType$) && + other is $SecondActivity$NullableType$; + } +} + +final class $SecondActivity$Type$ extends jni$_.JType { + + + @jni$_.internal + const $SecondActivity$Type$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/example/android_launch_activity/SecondActivity;'; + + @jni$_.internal + @core$_.override + SecondActivity fromReference(jni$_.JReference reference) => + SecondActivity.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $SecondActivity$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SecondActivity$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SecondActivity$Type$) && + other is $SecondActivity$Type$; + } +} + diff --git a/native_interop_demos/android_launch_activity/lib/main.dart b/native_interop_demos/android_launch_activity/lib/main.dart new file mode 100644 index 0000000..8a7e8eb --- /dev/null +++ b/native_interop_demos/android_launch_activity/lib/main.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +// uses added namespace because some bindings conflict with Dart classes +import 'package:android_launch_activity/gen/android.g.dart' as native; +import 'package:jni/jni.dart'; + +// Context.fromReference ensures we get Android Context object +// rather than the default `JObject` +var context = native.Context.fromReference(Jni.androidApplicationContext.reference); + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + // SECTION 2: START COPYING HERE + void _launchAndroidActivity() { + + var intent = native.Intent.new$1(context, native.SecondActivity.type.jClass); + intent.addFlags(native.Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra$18('message'.toJString(), 'Hello from Flutter'.toJString()); + context.startActivity(intent); + + intent.release(); + +} + // SECTION 2: END COPYING HERE + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: const Center(child: Text('Hello World!')), + floatingActionButton: FloatingActionButton( + // SECTION 3: Call `_launchAndroidActivity` somewhere. + onPressed: _launchAndroidActivity, + + // SECTION 3: End + tooltip: 'Launch Android activity', + child: const Icon(Icons.launch), + ), + ), + ); + } +} diff --git a/native_interop_demos/android_launch_activity/pubspec.yaml b/native_interop_demos/android_launch_activity/pubspec.yaml new file mode 100644 index 0000000..60107b2 --- /dev/null +++ b/native_interop_demos/android_launch_activity/pubspec.yaml @@ -0,0 +1,26 @@ +name: android_launch_activity +description: "A new Flutter project." +publish_to: 'none' # Remove this line if you wish to publish to pub.dev +version: 1.0.0+1 + +environment: + sdk: ^3.10.8 + +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + jni: ^0.15.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^6.0.0 + jnigen: ^0.15.0 + +flutter: + uses-material-design: true diff --git a/native_interop_demos/android_launch_activity/tool/jnigen.dart b/native_interop_demos/android_launch_activity/tool/jnigen.dart new file mode 100644 index 0000000..f3a34ae --- /dev/null +++ b/native_interop_demos/android_launch_activity/tool/jnigen.dart @@ -0,0 +1,27 @@ +import 'dart:io'; + +import 'package:jnigen/jnigen.dart'; + +void main(List args) { + final packageRoot = Platform.script.resolve('../'); + generateJniBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: packageRoot.resolve('lib/gen/android.g.dart'), + structure: OutputStructure.singleFile, + ), + ), + androidSdkConfig: AndroidSdkConfig(addGradleDeps: true), + sourcePath: [packageRoot.resolve('android/src/main/kotlin/com/example/android_launch_activity')], + classes: [ + // provided by Android OS + 'android.os.Bundle', + 'android.content.Intent', + 'android.content.Context', + // Needed so it can be referenced from Dart + 'com.example.android_launch_activity.SecondActivity' + ], + ), + ); +} diff --git a/native_interop_demos/android_toast_demo/.gitignore b/native_interop_demos/android_toast_demo/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/native_interop_demos/android_toast_demo/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/native_interop_demos/android_toast_demo/.metadata b/native_interop_demos/android_toast_demo/.metadata new file mode 100644 index 0000000..abb4ead --- /dev/null +++ b/native_interop_demos/android_toast_demo/.metadata @@ -0,0 +1,30 @@ +# 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: "67323de285b00232883f53b84095eb72be97d35c" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 67323de285b00232883f53b84095eb72be97d35c + base_revision: 67323de285b00232883f53b84095eb72be97d35c + - platform: android + create_revision: 67323de285b00232883f53b84095eb72be97d35c + base_revision: 67323de285b00232883f53b84095eb72be97d35c + + # 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' diff --git a/native_interop_demos/android_toast_demo/README.md b/native_interop_demos/android_toast_demo/README.md new file mode 100644 index 0000000..d09c86e --- /dev/null +++ b/native_interop_demos/android_toast_demo/README.md @@ -0,0 +1,27 @@ +# Android Toast Message Example + +This example shows how to initiate an Android `Toast` message from Dart directly without using a plugin. + +### How to run this example: + +- Run `flutter run` to run the app. + +- To regenerate bindings after changing Java code, run + `dart run tool/jnigen.dart`. This requires at least one APK build to have + been run before, so that JNIgen can obtain classpaths of Android Gradle + libraries. Therefore, run `flutter build apk` once before generating bindings + for the first time, or after a `flutter clean`. + +### General steps + +These are general steps to integrate Java code into a Flutter project using +JNIgen. + +- Create a JNIgen configuration script like `tool/jnigen.dart` in this + example. +- Generate bindings by running that script with `dart run`. +- To prevent tree shaking of your custom classes (which are always accessed + reflectively in JNI) use either the `@Keep` annotation + (`androidx.annotation.Keep`) for code written in the application itself, or + a proguard-rules file. +- Build and run the app. diff --git a/native_interop_demos/android_toast_demo/analysis_options.yaml b/native_interop_demos/android_toast_demo/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/native_interop_demos/android_toast_demo/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/native_interop_demos/android_toast_demo/android/.gitignore b/native_interop_demos/android_toast_demo/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/native_interop_demos/android_toast_demo/android/app/build.gradle.kts b/native_interop_demos/android_toast_demo/android/app/build.gradle.kts new file mode 100644 index 0000000..6f8e088 --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/app/build.gradle.kts @@ -0,0 +1,50 @@ +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") +} + +android { + namespace = "com.example.android_toast_demo" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.android_toast_demo" + // 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 + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} + +val emoji2_version = "1.3.0" + +dependencies { + implementation("androidx.emoji2:emoji2:$emoji2_version") +} diff --git a/native_interop_demos/android_toast_demo/android/app/src/debug/AndroidManifest.xml b/native_interop_demos/android_toast_demo/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/AndroidManifest.xml b/native_interop_demos/android_toast_demo/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b8aa998 --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/kotlin/com/example/android_toast_demo/MainActivity.kt b/native_interop_demos/android_toast_demo/android/app/src/main/kotlin/com/example/android_toast_demo/MainActivity.kt new file mode 100644 index 0000000..edfdea9 --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/app/src/main/kotlin/com/example/android_toast_demo/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.android_toast_demo + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/res/drawable-v21/launch_background.xml b/native_interop_demos/android_toast_demo/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/res/drawable/launch_background.xml b/native_interop_demos/android_toast_demo/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/native_interop_demos/android_toast_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/res/values-night/styles.xml b/native_interop_demos/android_toast_demo/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/native_interop_demos/android_toast_demo/android/app/src/main/res/values/styles.xml b/native_interop_demos/android_toast_demo/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/native_interop_demos/android_toast_demo/android/app/src/profile/AndroidManifest.xml b/native_interop_demos/android_toast_demo/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/native_interop_demos/android_toast_demo/android/build.gradle.kts b/native_interop_demos/android_toast_demo/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/build.gradle.kts @@ -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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/native_interop_demos/android_toast_demo/android/gradle.properties b/native_interop_demos/android_toast_demo/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/native_interop_demos/android_toast_demo/android/gradle/wrapper/gradle-wrapper.properties b/native_interop_demos/android_toast_demo/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/gradle/wrapper/gradle-wrapper.properties @@ -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-all.zip diff --git a/native_interop_demos/android_toast_demo/android/settings.gradle.kts b/native_interop_demos/android_toast_demo/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/native_interop_demos/android_toast_demo/android/settings.gradle.kts @@ -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.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/native_interop_demos/android_toast_demo/lib/gen/android/widget/Toast.dart b/native_interop_demos/android_toast_demo/lib/gen/android/widget/Toast.dart new file mode 100644 index 0000000..d2bcbc5 --- /dev/null +++ b/native_interop_demos/android_toast_demo/lib/gen/android/widget/Toast.dart @@ -0,0 +1,599 @@ +// AUTO GENERATED BY JNIGEN 0.15.0. DO NOT EDIT! + + +// ignore_for_file: annotate_overrides +// ignore_for_file: argument_type_not_assignable +// ignore_for_file: camel_case_extensions +// ignore_for_file: camel_case_types +// ignore_for_file: constant_identifier_names +// ignore_for_file: comment_references +// ignore_for_file: doc_directive_unknown +// ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter +// ignore_for_file: invalid_internal_annotation +// ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: library_prefixes +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: no_leading_underscores_for_library_prefixes +// ignore_for_file: no_leading_underscores_for_local_identifiers +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: only_throw_errors +// ignore_for_file: overridden_fields +// ignore_for_file: prefer_double_quotes +// ignore_for_file: unintended_html_in_doc_comment +// ignore_for_file: unnecessary_cast +// ignore_for_file: unnecessary_non_null_assertion +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: unused_element +// ignore_for_file: unused_field +// ignore_for_file: unused_import +// ignore_for_file: unused_local_variable +// ignore_for_file: unused_shown_name +// ignore_for_file: use_super_parameters + + +import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; + +import 'package:jni/_internal.dart' as jni$_; +import 'package:jni/jni.dart' as jni$_; + + +/// from: `android.widget.Toast$Callback` +class Toast$Callback extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + + + @jni$_.internal + Toast$Callback.fromReference( + + jni$_.JReference reference, + ) : + $type = type, + super.fromReference( + + reference + ); + + static final _class = jni$_.JClass.forName(r'android/widget/Toast$Callback'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Toast$Callback$NullableType$(); + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Toast$Callback$Type$(); + static final _id_onToastHidden = _class.instanceMethodId( + r'onToastHidden', + r'()V', + ); + + static final _onToastHidden = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void onToastHidden()` + void onToastHidden(){ + + + _onToastHidden(reference.pointer, _id_onToastHidden as jni$_.JMethodIDPtr).check(); + } + + static final _id_onToastShown = _class.instanceMethodId( + r'onToastShown', + r'()V', + ); + + static final _onToastShown = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void onToastShown()` + void onToastShown(){ + + + _onToastShown(reference.pointer, _id_onToastShown as jni$_.JMethodIDPtr).check(); + } + +} +final class $Toast$Callback$NullableType$ extends jni$_.JType { + + + @jni$_.internal + const $Toast$Callback$NullableType$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/widget/Toast$Callback;'; + + @jni$_.internal + @core$_.override + Toast$Callback? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Toast$Callback.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Toast$Callback$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Toast$Callback$NullableType$) && + other is $Toast$Callback$NullableType$; + } +} + +final class $Toast$Callback$Type$ extends jni$_.JType { + + + @jni$_.internal + const $Toast$Callback$Type$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/widget/Toast$Callback;'; + + @jni$_.internal + @core$_.override + Toast$Callback fromReference(jni$_.JReference reference) => + Toast$Callback.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Toast$Callback$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Toast$Callback$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Toast$Callback$Type$) && + other is $Toast$Callback$Type$; + } +} + +/// from: `android.widget.Toast` +class Toast extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + + + @jni$_.internal + Toast.fromReference( + + jni$_.JReference reference, + ) : + $type = type, + super.fromReference( + + reference + ); + + static final _class = jni$_.JClass.forName(r'android/widget/Toast'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Toast$NullableType$(); + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Toast$Type$(); + /// from: `static public final int LENGTH_LONG` + static const LENGTH_LONG = 1; + /// from: `static public final int LENGTH_SHORT` + static const LENGTH_SHORT = 0; + static final _id_new$ = _class.constructorId( + r'(Landroid/content/Context;)V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_NewObject') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void (android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + factory Toast(jni$_.JObject? context, ) { + + final _$context = context?.reference ?? jni$_.jNullReference; + return Toast.fromReference( + + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr, _$context.pointer).reference + ); + } + + static final _id_addCallback = _class.instanceMethodId( + r'addCallback', + r'(Landroid/widget/Toast$Callback;)V', + ); + + static final _addCallback = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void addCallback(android.widget.Toast$Callback callback)` + void addCallback(Toast$Callback? callback, ){ + + final _$callback = callback?.reference ?? jni$_.jNullReference; + _addCallback(reference.pointer, _id_addCallback as jni$_.JMethodIDPtr, _$callback.pointer).check(); + } + + static final _id_cancel = _class.instanceMethodId( + r'cancel', + r'()V', + ); + + static final _cancel = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void cancel()` + void cancel(){ + + + _cancel(reference.pointer, _id_cancel as jni$_.JMethodIDPtr).check(); + } + + static final _id_getDuration = _class.instanceMethodId( + r'getDuration', + r'()I', + ); + + static final _getDuration = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int getDuration()` + int getDuration(){ + + + return _getDuration(reference.pointer, _id_getDuration as jni$_.JMethodIDPtr).integer; + } + + static final _id_getGravity = _class.instanceMethodId( + r'getGravity', + r'()I', + ); + + static final _getGravity = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int getGravity()` + int getGravity(){ + + + return _getGravity(reference.pointer, _id_getGravity as jni$_.JMethodIDPtr).integer; + } + + static final _id_getHorizontalMargin = _class.instanceMethodId( + r'getHorizontalMargin', + r'()F', + ); + + static final _getHorizontalMargin = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallFloatMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public float getHorizontalMargin()` + double getHorizontalMargin(){ + + + return _getHorizontalMargin(reference.pointer, _id_getHorizontalMargin as jni$_.JMethodIDPtr).float; + } + + static final _id_getVerticalMargin = _class.instanceMethodId( + r'getVerticalMargin', + r'()F', + ); + + static final _getVerticalMargin = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallFloatMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public float getVerticalMargin()` + double getVerticalMargin(){ + + + return _getVerticalMargin(reference.pointer, _id_getVerticalMargin as jni$_.JMethodIDPtr).float; + } + + static final _id_getView = _class.instanceMethodId( + r'getView', + r'()Landroid/view/View;', + ); + + static final _getView = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallObjectMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public android.view.View getView()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getView(){ + + + return _getView(reference.pointer, _id_getView as jni$_.JMethodIDPtr).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getXOffset = _class.instanceMethodId( + r'getXOffset', + r'()I', + ); + + static final _getXOffset = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int getXOffset()` + int getXOffset(){ + + + return _getXOffset(reference.pointer, _id_getXOffset as jni$_.JMethodIDPtr).integer; + } + + static final _id_getYOffset = _class.instanceMethodId( + r'getYOffset', + r'()I', + ); + + static final _getYOffset = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallIntMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public int getYOffset()` + int getYOffset(){ + + + return _getYOffset(reference.pointer, _id_getYOffset as jni$_.JMethodIDPtr).integer; + } + + static final _id_makeText = _class.staticMethodId( + r'makeText', + r'(Landroid/content/Context;II)Landroid/widget/Toast;', + ); + + static final _makeText = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, int, int)>(); + /// from: `static public android.widget.Toast makeText(android.content.Context context, int i, int i1)` + /// The returned object must be released after use, by calling the [release] method. + static Toast? makeText(jni$_.JObject? context, int i, int i1, ){ + + final _$context = context?.reference ?? jni$_.jNullReference; + return _makeText(_class.reference.pointer, _id_makeText as jni$_.JMethodIDPtr, _$context.pointer, i, i1).object(const $Toast$NullableType$()); + } + + static final _id_makeText$1 = _class.staticMethodId( + r'makeText', + r'(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;', + ); + + static final _makeText$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer, jni$_.Pointer, jni$_.Int32)>)>>('globalEnv_CallStaticObjectMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer, jni$_.Pointer, int)>(); + /// from: `static public android.widget.Toast makeText(android.content.Context context, java.lang.CharSequence charSequence, int i)` + /// The returned object must be released after use, by calling the [release] method. + static Toast? makeText$1(jni$_.JObject? context, jni$_.JObject? charSequence, int i, ){ + + final _$context = context?.reference ?? jni$_.jNullReference; + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + return _makeText$1(_class.reference.pointer, _id_makeText$1 as jni$_.JMethodIDPtr, _$context.pointer, _$charSequence.pointer, i).object(const $Toast$NullableType$()); + } + + static final _id_removeCallback = _class.instanceMethodId( + r'removeCallback', + r'(Landroid/widget/Toast$Callback;)V', + ); + + static final _removeCallback = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void removeCallback(android.widget.Toast$Callback callback)` + void removeCallback(Toast$Callback? callback, ){ + + final _$callback = callback?.reference ?? jni$_.jNullReference; + _removeCallback(reference.pointer, _id_removeCallback as jni$_.JMethodIDPtr, _$callback.pointer).check(); + } + + static final _id_setDuration = _class.instanceMethodId( + r'setDuration', + r'(I)V', + ); + + static final _setDuration = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public void setDuration(int i)` + void setDuration(int i, ){ + + + _setDuration(reference.pointer, _id_setDuration as jni$_.JMethodIDPtr, i).check(); + } + + static final _id_setGravity = _class.instanceMethodId( + r'setGravity', + r'(III)V', + ); + + static final _setGravity = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32, jni$_.Int32, jni$_.Int32)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, int, int, int)>(); + /// from: `public void setGravity(int i, int i1, int i2)` + void setGravity(int i, int i1, int i2, ){ + + + _setGravity(reference.pointer, _id_setGravity as jni$_.JMethodIDPtr, i, i1, i2).check(); + } + + static final _id_setMargin = _class.instanceMethodId( + r'setMargin', + r'(FF)V', + ); + + static final _setMargin = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Double, jni$_.Double)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, double, double)>(); + /// from: `public void setMargin(float f, float f1)` + void setMargin(double f, double f1, ){ + + + _setMargin(reference.pointer, _id_setMargin as jni$_.JMethodIDPtr, f, f1).check(); + } + + static final _id_setText = _class.instanceMethodId( + r'setText', + r'(I)V', + ); + + static final _setText = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, int)>(); + /// from: `public void setText(int i)` + void setText(int i, ){ + + + _setText(reference.pointer, _id_setText as jni$_.JMethodIDPtr, i).check(); + } + + static final _id_setText$1 = _class.instanceMethodId( + r'setText', + r'(Ljava/lang/CharSequence;)V', + ); + + static final _setText$1 = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void setText(java.lang.CharSequence charSequence)` + void setText$1(jni$_.JObject? charSequence, ){ + + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + _setText$1(reference.pointer, _id_setText$1 as jni$_.JMethodIDPtr, _$charSequence.pointer).check(); + } + + static final _id_setView = _class.instanceMethodId( + r'setView', + r'(Landroid/view/View;)V', + ); + + static final _setView = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Pointer,)>)>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, jni$_.Pointer)>(); + /// from: `public void setView(android.view.View view)` + void setView(jni$_.JObject? view, ){ + + final _$view = view?.reference ?? jni$_.jNullReference; + _setView(reference.pointer, _id_setView as jni$_.JMethodIDPtr, _$view.pointer).check(); + } + + static final _id_show = _class.instanceMethodId( + r'show', + r'()V', + ); + + static final _show = jni$_.ProtectedJniExtensions + .lookup, jni$_.JMethodIDPtr, )>>('globalEnv_CallVoidMethod') + .asFunction, jni$_.JMethodIDPtr, )>(); + /// from: `public void show()` + void show(){ + + + _show(reference.pointer, _id_show as jni$_.JMethodIDPtr).check(); + } + +} +final class $Toast$NullableType$ extends jni$_.JType { + + + @jni$_.internal + const $Toast$NullableType$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/widget/Toast;'; + + @jni$_.internal + @core$_.override + Toast? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Toast.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Toast$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Toast$NullableType$) && + other is $Toast$NullableType$; + } +} + +final class $Toast$Type$ extends jni$_.JType { + + + @jni$_.internal + const $Toast$Type$( + + ); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/widget/Toast;'; + + @jni$_.internal + @core$_.override + Toast fromReference(jni$_.JReference reference) => + Toast.fromReference( + + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Toast$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Toast$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Toast$Type$) && + other is $Toast$Type$; + } +} + + diff --git a/native_interop_demos/android_toast_demo/lib/gen/android/widget/_package.dart b/native_interop_demos/android_toast_demo/lib/gen/android/widget/_package.dart new file mode 100644 index 0000000..4048649 --- /dev/null +++ b/native_interop_demos/android_toast_demo/lib/gen/android/widget/_package.dart @@ -0,0 +1,2 @@ +// AUTO GENERATED BY JNIGEN 0.15.0. DO NOT EDIT! +export 'Toast.dart'; \ No newline at end of file diff --git a/native_interop_demos/android_toast_demo/lib/main.dart b/native_interop_demos/android_toast_demo/lib/main.dart new file mode 100644 index 0000000..5671e70 --- /dev/null +++ b/native_interop_demos/android_toast_demo/lib/main.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:android_toast_demo/gen/android/os/_package.dart'; +import 'package:android_toast_demo/gen/android/widget/_package.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:jni/jni.dart'; + + +JObject context = Jni.androidApplicationContext; + + +/// Display DateTime retrieved from Dart +void showToast() { + final message = 'The time is now ${DateTime.now()}'; + +// Corresponds to this second signature +// public static Toast makeText (Context context, +// CharSequence text, +// int duration) +// First one uses R namespace resources + Toast.makeText$1(context, message.toJString(), Toast.LENGTH_LONG)!.show(); +} + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData(primarySwatch: Colors.teal), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatelessWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + child: const Text('Show Time'), + onPressed: () => showToast(), + ), + ], + ), + ), + ); + } +} diff --git a/native_interop_demos/android_toast_demo/pubspec.yaml b/native_interop_demos/android_toast_demo/pubspec.yaml new file mode 100644 index 0000000..767479a --- /dev/null +++ b/native_interop_demos/android_toast_demo/pubspec.yaml @@ -0,0 +1,28 @@ +name: android_toast_demo +description: "Demonstrates jnigen with Android API calls" +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.0.0+1 + +environment: + sdk: ^3.10.8 + +dependencies: + flutter: + sdk: flutter + + cupertino_icons: ^1.0.8 + jni: ^0.15.2 + jnigen: ^0.15.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^6.0.0 + +flutter: + + uses-material-design: true diff --git a/native_interop_demos/android_toast_demo/tool/jnigen.dart b/native_interop_demos/android_toast_demo/tool/jnigen.dart new file mode 100644 index 0000000..6f787bf --- /dev/null +++ b/native_interop_demos/android_toast_demo/tool/jnigen.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +import 'package:jnigen/jnigen.dart'; + +void main(List args) { + final packageRoot = Platform.script.resolve('../'); + generateJniBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: packageRoot.resolve('lib/gen/'), + structure: OutputStructure.packageStructure, + ), + ), + androidSdkConfig: AndroidSdkConfig(addGradleDeps: true), + classes: [ + 'android.widget.Toast', // provided by Android OS + ], + ), + ); +} diff --git a/native_interop_demos/graalvm_test_native_interop/.gitignore b/native_interop_demos/graalvm_test_native_interop/.gitignore new file mode 100644 index 0000000..3a85790 --- /dev/null +++ b/native_interop_demos/graalvm_test_native_interop/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/native_interop_demos/graalvm_test_native_interop/README.md b/native_interop_demos/graalvm_test_native_interop/README.md new file mode 100644 index 0000000..38fb7d1 --- /dev/null +++ b/native_interop_demos/graalvm_test_native_interop/README.md @@ -0,0 +1,53 @@ +# GraalVM Test + +This is a sample command-line application demonstrating how to use Dart's Java Native Interface (JNI) packages +(`jni` and `jnigen`) to interoperate with Java code, specifically the GraalVM Polyglot API. + +The application initializes a Java Virtual Machine (JVM) within a Dart environment, loads the necessary GraalVM JAR +files, and then executes a simple JavaScript snippet using GraalVM's polyglot capabilities. + +## How it Works + +The core logic is in `bin/graalvm_test.dart`. It performs the following steps: + +1. **Spawns a JVM**: It uses `Jni.spawn()` to start a JVM, providing the paths to the required GraalVM JAR files +located in the `mvn_jar` directory. +2. **Creates a Polyglot Context**: It creates a GraalVM `Context` for the JavaScript language. +3. **Executes JavaScript**: It evaluates a JavaScript string that defines a simple function. +4. **Interoperates**: It calls the JavaScript function from Dart, passing a string argument ("World") to it. +The JavaScript code then prints a message to the console. + +The Dart bindings for the GraalVM Polyglot API (`org.graalvm.polyglot.Context` and `org.graalvm.polyglot.Value`) are +generated by `jnigen` based on the configuration in `jnigen.yaml` and the Java source files in `mvn_java`. + +## Requirements + +* Dart SDK +* Java Development Kit (JDK) 17 + +## Setup + +1. **Get dependencies**: + ```bash + dart pub get + ``` + +2. **Generate JNI bindings**: + This project uses `jnigen` to generate the Dart code for Java classes. + Run the following command to generate the necessary files: + ```bash + dart run jni:setup + dart run jnigen:setup + dart run jnigen --config jnigen.yaml + ``` + The underlying graalvm libraries require OpenJDK 17 or higher. + +## Running the Application + +After completing the setup steps, run the application with the following command: + +```bash +dart run bin/graalvm_test.dart +``` + +You should see output from both Dart and the executed JavaScript code in your console. \ No newline at end of file diff --git a/native_interop_demos/graalvm_test_native_interop/analysis_options.yaml b/native_interop_demos/graalvm_test_native_interop/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/native_interop_demos/graalvm_test_native_interop/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/native_interop_demos/graalvm_test_native_interop/bin/graalvm_test.dart b/native_interop_demos/graalvm_test_native_interop/bin/graalvm_test.dart new file mode 100644 index 0000000..b6293e2 --- /dev/null +++ b/native_interop_demos/graalvm_test_native_interop/bin/graalvm_test.dart @@ -0,0 +1,35 @@ +import 'package:jni/jni.dart'; +import 'package:path/path.dart'; +import 'package:graalvm_test/graal/org/graalvm/polyglot/_package.dart' as graal; + +void main(List arguments) { + Jni.spawn( + dylibDir: join('build', 'jni_libs'), + classPath: [ + './mvn_jar/collections-24.2.2.jar', + './mvn_jar/icu4j-24.2.2.jar', + './mvn_jar/jniutils-24.2.2.jar', + './mvn_jar/js-language-24.2.2.jar', + './mvn_jar/nativebridge-24.2.2.jar', + './mvn_jar/nativeimage-24.2.2.jar', + './mvn_jar/polyglot-24.2.2.jar', + './mvn_jar/regex-24.2.2.jar', + './mvn_jar/truffle-api-24.2.2.jar', + './mvn_jar/truffle-compiler-24.2.2.jar', + './mvn_jar/truffle-enterprise-24.2.2.jar', + './mvn_jar/truffle-runtime-24.2.2.jar', + './mvn_jar/word-24.2.2.jar', + './mvn_jar/xz-24.2.2.jar' + ], + ); + + var jsCode = "(function myFun(param){console.log('Hello ' + param + ' from JS');})"; + + var langs = JArray.of(JString.type, ["js".toJString()]); + var context = graal.Context.create(langs); + var value = context?.eval$1("js".toJString(), jsCode.toJString()); + print(value); + value?.execute(JArray.of(JString.type, ["World".toJString()])); + return; +} + diff --git a/native_interop_demos/graalvm_test_native_interop/jnigen.yaml b/native_interop_demos/graalvm_test_native_interop/jnigen.yaml new file mode 100644 index 0000000..09f367c --- /dev/null +++ b/native_interop_demos/graalvm_test_native_interop/jnigen.yaml @@ -0,0 +1,12 @@ +output: + dart: + path: lib/graal/ + +classes: + - 'org.graalvm.polyglot.Context' + - 'org.graalvm.polyglot.Value' + +maven_downloads: + jar_only_deps: + - 'org.graalvm.polyglot:polyglot:24.2.1' + - 'org.graalvm.polyglot:js:24.2.2' diff --git a/native_interop_demos/graalvm_test_native_interop/lib/graalvm_test.dart b/native_interop_demos/graalvm_test_native_interop/lib/graalvm_test.dart new file mode 100644 index 0000000..f64ad72 --- /dev/null +++ b/native_interop_demos/graalvm_test_native_interop/lib/graalvm_test.dart @@ -0,0 +1,3 @@ +int calculate() { + return 6 * 7; +} diff --git a/native_interop_demos/graalvm_test_native_interop/pubspec.yaml b/native_interop_demos/graalvm_test_native_interop/pubspec.yaml new file mode 100644 index 0000000..816bcbe --- /dev/null +++ b/native_interop_demos/graalvm_test_native_interop/pubspec.yaml @@ -0,0 +1,17 @@ +name: graalvm_test +description: A sample command-line application. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.10.0-36.0.dev + +# Add regular dependencies here. +dependencies: + path: ^1.9.0 + jni: ^0.14.2 + jnigen: ^0.14.2 + +dev_dependencies: + lints: ^6.0.0 + test: ^1.25.6 diff --git a/native_interop_demos/ios_calendar_demo/.gitignore b/native_interop_demos/ios_calendar_demo/.gitignore new file mode 100644 index 0000000..79f7eca --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/.gitignore @@ -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 + +# Widget Preview related +.widget_preview/ diff --git a/native_interop_demos/ios_calendar_demo/.metadata b/native_interop_demos/ios_calendar_demo/.metadata new file mode 100644 index 0000000..d299546 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/.metadata @@ -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: "e1e42652202b428d9e9eaa91fe00d5952587e7ca" + channel: "master" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e1e42652202b428d9e9eaa91fe00d5952587e7ca + base_revision: e1e42652202b428d9e9eaa91fe00d5952587e7ca + - platform: ios + create_revision: e1e42652202b428d9e9eaa91fe00d5952587e7ca + base_revision: e1e42652202b428d9e9eaa91fe00d5952587e7ca + - platform: macos + create_revision: e1e42652202b428d9e9eaa91fe00d5952587e7ca + base_revision: e1e42652202b428d9e9eaa91fe00d5952587e7ca + + # 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' diff --git a/native_interop_demos/ios_calendar_demo/README.md b/native_interop_demos/ios_calendar_demo/README.md new file mode 100644 index 0000000..5e65e60 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/README.md @@ -0,0 +1,11 @@ +# ios_calendar_demo + +A demonstration of using Dart native interop to communicate with the iOS Calendar. + +## To build + +```sh +dart run tool/generate_code.dart # To build Dart bindings +..... # Start a simulator +flutter run +``` diff --git a/native_interop_demos/ios_calendar_demo/analysis_options.yaml b/native_interop_demos/ios_calendar_demo/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/native_interop_demos/ios_calendar_demo/ios/.gitignore b/native_interop_demos/ios_calendar_demo/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +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 diff --git a/native_interop_demos/ios_calendar_demo/ios/Flutter/AppFrameworkInfo.plist b/native_interop_demos/ios_calendar_demo/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Flutter/Debug.xcconfig b/native_interop_demos/ios_calendar_demo/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/native_interop_demos/ios_calendar_demo/ios/Flutter/Release.xcconfig b/native_interop_demos/ios_calendar_demo/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.pbxproj b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a962af5 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,644 @@ +// !$*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 */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + 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 = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + 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; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + 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 */ + 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"; + }; +/* 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 */, + 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.iosCalendarDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.iosCalendarDemo.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.iosCalendarDemo.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.iosCalendarDemo.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + 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; + "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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.iosCalendarDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.iosCalendarDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner.xcworkspace/contents.xcworkspacedata b/native_interop_demos/ios_calendar_demo/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native_interop_demos/ios_calendar_demo/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/native_interop_demos/ios_calendar_demo/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/AppDelegate.swift b/native_interop_demos/ios_calendar_demo/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Base.lproj/LaunchScreen.storyboard b/native_interop_demos/ios_calendar_demo/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Base.lproj/Main.storyboard b/native_interop_demos/ios_calendar_demo/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Info.plist b/native_interop_demos/ios_calendar_demo/ios/Runner/Info.plist new file mode 100644 index 0000000..e054c73 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner/Info.plist @@ -0,0 +1,72 @@ + + + + + NSCalendarsFullAccessUsageDescription + This app needs calendar access to create and manage your events. + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Ios Calendar Demo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ios_calendar_demo + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/Runner-Bridging-Header.h b/native_interop_demos/ios_calendar_demo/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/native_interop_demos/ios_calendar_demo/ios/Runner/SceneDelegate.swift b/native_interop_demos/ios_calendar_demo/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/native_interop_demos/ios_calendar_demo/ios/RunnerTests/RunnerTests.swift b/native_interop_demos/ios_calendar_demo/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/native_interop_demos/ios_calendar_demo/lib/create_event_dialog.dart b/native_interop_demos/ios_calendar_demo/lib/create_event_dialog.dart new file mode 100644 index 0000000..5294d8d --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/lib/create_event_dialog.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:objective_c/objective_c.dart'; + +import 'eventkit_bindings.dart'; + +class CreateEventDialog extends StatefulWidget { + final EKEventStore eventStore; + final VoidCallback onEventCreated; + + const CreateEventDialog({ + super.key, + required this.eventStore, + required this.onEventCreated, + }); + + @override + State createState() => _CreateEventDialogState(); +} + +class _CreateEventDialogState extends State { + final _titleController = TextEditingController(); + final _notesController = TextEditingController(); + DateTime _startDate = DateTime.now(); + DateTime _endDate = DateTime.now().add(const Duration(hours: 1)); + + Future _selectDateTime(BuildContext context, bool isStart) async { + final initialDate = isStart ? _startDate : _endDate; + final pickedDate = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + if (pickedDate == null) return; + + if (!context.mounted) return; + + final pickedTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(initialDate), + ); + if (pickedTime == null) return; + + final finalDateTime = DateTime( + pickedDate.year, + pickedDate.month, + pickedDate.day, + pickedTime.hour, + pickedTime.minute, + ); + + setState(() { + if (isStart) { + _startDate = finalDateTime; + if (_endDate.isBefore(_startDate)) { + _endDate = _startDate.add(const Duration(hours: 1)); + } + } else { + _endDate = finalDateTime; + } + }); + } + + void _saveEvent() { + final event = EKEvent.eventWithEventStore(widget.eventStore); + event.title = _titleController.text.toNSString(); + event.notes = _notesController.text.toNSString(); + event.startDate = _startDate.toNSDate(); + event.endDate = _endDate.toNSDate(); + + final defaultCalendar = widget.eventStore.defaultCalendarForNewEvents; + if (defaultCalendar != null) { + event.calendar = defaultCalendar; + final success = widget.eventStore.saveEvent( + event, + span: EKSpan.EKSpanThisEvent, + commit: true, + ); + if (success) { + widget.onEventCreated(); + } else { + debugPrint('Failed to save event'); + } + } else { + debugPrint('No default calendar found'); + } + + Navigator.of(context).pop(); + } + + @override + void dispose() { + _titleController.dispose(); + _notesController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Create Event'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _titleController, + decoration: const InputDecoration(labelText: 'Title'), + ), + TextField( + controller: _notesController, + decoration: const InputDecoration(labelText: 'Notes'), + maxLines: 3, + ), + ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Start Date'), + subtitle: Text('${_startDate.year}-${_startDate.month.toString().padLeft(2, '0')}-${_startDate.day.toString().padLeft(2, '0')} ${_startDate.hour.toString().padLeft(2, '0')}:${_startDate.minute.toString().padLeft(2, '0')}'), + onTap: () => _selectDateTime(context, true), + trailing: const Icon(Icons.calendar_today), + ), + ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('End Date'), + subtitle: Text('${_endDate.year}-${_endDate.month.toString().padLeft(2, '0')}-${_endDate.day.toString().padLeft(2, '0')} ${_endDate.hour.toString().padLeft(2, '0')}:${_endDate.minute.toString().padLeft(2, '0')}'), + onTap: () => _selectDateTime(context, false), + trailing: const Icon(Icons.calendar_today), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: _saveEvent, + child: const Text('Save'), + ), + ], + ); + } +} diff --git a/native_interop_demos/ios_calendar_demo/lib/eventkit_bindings.dart.m b/native_interop_demos/ios_calendar_demo/lib/eventkit_bindings.dart.m new file mode 100644 index 0000000..2ef47c3 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/lib/eventkit_bindings.dart.m @@ -0,0 +1,122 @@ +#include +#import +#import +#import + +#if !__has_feature(objc_arc) +#error "This file must be compiled with ARC enabled" +#endif + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + +typedef struct { + int64_t version; + void* (*newWaiter)(void); + void (*awaitWaiter)(void*); + void* (*currentIsolate)(void); + void (*enterIsolate)(void*); + void (*exitIsolate)(void); + int64_t (*getMainPortId)(void); + bool (*getCurrentThreadOwnsIsolate)(int64_t); +} DOBJC_Context; + +id objc_retainBlock(id); + +#define BLOCKING_BLOCK_IMPL(ctx, BLOCK_SIG, INVOKE_DIRECT, INVOKE_LISTENER) \ + assert(ctx->version >= 1); \ + void* targetIsolate = ctx->currentIsolate(); \ + int64_t targetPort = ctx->getMainPortId == NULL ? 0 : ctx->getMainPortId(); \ + return BLOCK_SIG { \ + void* currentIsolate = ctx->currentIsolate(); \ + bool mayEnterIsolate = \ + currentIsolate == NULL && \ + ctx->getCurrentThreadOwnsIsolate != NULL && \ + ctx->getCurrentThreadOwnsIsolate(targetPort); \ + if (currentIsolate == targetIsolate || mayEnterIsolate) { \ + if (mayEnterIsolate) { \ + ctx->enterIsolate(targetIsolate); \ + } \ + INVOKE_DIRECT; \ + if (mayEnterIsolate) { \ + ctx->exitIsolate(); \ + } \ + } else { \ + void* waiter = ctx->newWaiter(); \ + INVOKE_LISTENER; \ + ctx->awaitWaiter(waiter); \ + } \ + }; + + +typedef void (^_ListenerTrampoline)(BOOL arg0, id arg1); +__attribute__((visibility("default"))) __attribute__((used)) +_ListenerTrampoline _NativeLibrary_wrapListenerBlock_hk7n97(_ListenerTrampoline block) NS_RETURNS_RETAINED { + return ^void(BOOL arg0, id arg1) { + objc_retainBlock(block); + block(arg0, (__bridge id)(__bridge_retained void*)(arg1)); + }; +} + +typedef void (^_BlockingTrampoline)(void * waiter, BOOL arg0, id arg1); +__attribute__((visibility("default"))) __attribute__((used)) +_ListenerTrampoline _NativeLibrary_wrapBlockingBlock_hk7n97( + _BlockingTrampoline block, _BlockingTrampoline listenerBlock, + DOBJC_Context* ctx) NS_RETURNS_RETAINED { + BLOCKING_BLOCK_IMPL(ctx, ^void(BOOL arg0, id arg1), { + objc_retainBlock(block); + block(nil, arg0, (__bridge id)(__bridge_retained void*)(arg1)); + }, { + objc_retainBlock(listenerBlock); + listenerBlock(waiter, arg0, (__bridge id)(__bridge_retained void*)(arg1)); + }); +} + +typedef void (^_ListenerTrampoline_1)(id arg0, BOOL * arg1); +__attribute__((visibility("default"))) __attribute__((used)) +_ListenerTrampoline_1 _NativeLibrary_wrapListenerBlock_t8l8el(_ListenerTrampoline_1 block) NS_RETURNS_RETAINED { + return ^void(id arg0, BOOL * arg1) { + objc_retainBlock(block); + block((__bridge id)(__bridge_retained void*)(arg0), arg1); + }; +} + +typedef void (^_BlockingTrampoline_1)(void * waiter, id arg0, BOOL * arg1); +__attribute__((visibility("default"))) __attribute__((used)) +_ListenerTrampoline_1 _NativeLibrary_wrapBlockingBlock_t8l8el( + _BlockingTrampoline_1 block, _BlockingTrampoline_1 listenerBlock, + DOBJC_Context* ctx) NS_RETURNS_RETAINED { + BLOCKING_BLOCK_IMPL(ctx, ^void(id arg0, BOOL * arg1), { + objc_retainBlock(block); + block(nil, (__bridge id)(__bridge_retained void*)(arg0), arg1); + }, { + objc_retainBlock(listenerBlock); + listenerBlock(waiter, (__bridge id)(__bridge_retained void*)(arg0), arg1); + }); +} + +typedef void (^_ListenerTrampoline_2)(id arg0); +__attribute__((visibility("default"))) __attribute__((used)) +_ListenerTrampoline_2 _NativeLibrary_wrapListenerBlock_xtuoz7(_ListenerTrampoline_2 block) NS_RETURNS_RETAINED { + return ^void(id arg0) { + objc_retainBlock(block); + block((__bridge id)(__bridge_retained void*)(arg0)); + }; +} + +typedef void (^_BlockingTrampoline_2)(void * waiter, id arg0); +__attribute__((visibility("default"))) __attribute__((used)) +_ListenerTrampoline_2 _NativeLibrary_wrapBlockingBlock_xtuoz7( + _BlockingTrampoline_2 block, _BlockingTrampoline_2 listenerBlock, + DOBJC_Context* ctx) NS_RETURNS_RETAINED { + BLOCKING_BLOCK_IMPL(ctx, ^void(id arg0), { + objc_retainBlock(block); + block(nil, (__bridge id)(__bridge_retained void*)(arg0)); + }, { + objc_retainBlock(listenerBlock); + listenerBlock(waiter, (__bridge id)(__bridge_retained void*)(arg0)); + }); +} +#undef BLOCKING_BLOCK_IMPL + +#pragma clang diagnostic pop diff --git a/native_interop_demos/ios_calendar_demo/lib/main.dart b/native_interop_demos/ios_calendar_demo/lib/main.dart new file mode 100644 index 0000000..6dd198c --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/lib/main.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:objective_c/objective_c.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'create_event_dialog.dart'; +import 'eventkit_bindings.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp(home: CalendarHomePage()); + } +} + +class CalendarHomePage extends StatefulWidget { + const CalendarHomePage({super.key}); + + @override + State createState() => _CalendarHomePageState(); +} + +class _CalendarHomePageState extends State { + bool _hasCalendarPermission = false; + List> _events = []; + late final EKEventStore _eventStore; + + @override + void initState() { + super.initState(); + _eventStore = EKEventStore(); + _checkPermission(); + } + + Future _checkPermission() async { + final status = await Permission.calendarFullAccess.status; + setState(() { + _hasCalendarPermission = status.isGranted; + }); + } + + Future _requestPermission() async { + await Permission.calendarFullAccess + .onGrantedCallback(() { + debugPrint('Granted'); + setState(() { + _hasCalendarPermission = true; + }); + }) + .onDeniedCallback(() { + debugPrint('denied'); + }) + .request(); + } + + void _retrieveEvents() { + final startDate = NSDate.date(); + final endDate = NSDate.dateWithTimeIntervalSinceNow(30.0 * 24 * 60 * 60); + + final calendars = _eventStore.calendars; + final predicate = _eventStore.predicateForEventsWithStartDate( + startDate, + endDate: endDate, + calendars: calendars, + ); + + final NSArray eventsArray = _eventStore.eventsMatchingPredicate(predicate); + + final count = eventsArray.count; + List fetchedEvents = []; + for (int i = 0; i < count; i++) { + final obj = eventsArray.objectAtIndex(i); + final event = EKEvent.as(obj); + fetchedEvents.add(event); + } + + fetchedEvents.sort((a, b) { + return a.startDate.compare(b.startDate).value; + }); + + setState(() { + _events = fetchedEvents.map((e) { + final title = e.title.toDartString(); + final dt = e.startDate.toDateTime(); + return { + 'title': title, + 'date': + '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}', + 'time': + '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}', + }; + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('EventKit Native Interop Demo')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: _hasCalendarPermission ? null : _requestPermission, + child: Text( + _hasCalendarPermission + ? 'Calendar Access Granted' + : 'Request Calendar Permission', + ), + ), + const Divider(height: 32), + ElevatedButton( + onPressed: _hasCalendarPermission + ? () => showDialog( + context: context, + builder: (context) => CreateEventDialog( + eventStore: _eventStore, + onEventCreated: _retrieveEvents, + ), + ) + : null, + child: const Text('Create Event'), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: _hasCalendarPermission ? _retrieveEvents : null, + child: const Text('Retrieve Events'), + ), + const SizedBox(height: 24), + const Text( + 'Events', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Expanded( + child: _events.isEmpty + ? const Center(child: Text('No events to display.')) + : SingleChildScrollView( + child: DataTable( + columns: const [ + DataColumn(label: Text('Title')), + DataColumn(label: Text('Date')), + DataColumn(label: Text('Time')), + ], + rows: _events + .map( + (event) => DataRow( + cells: [ + DataCell(Text(event['title'] ?? '')), + DataCell(Text(event['date'] ?? '')), + DataCell(Text(event['time'] ?? '')), + ], + ), + ) + .toList(), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/native_interop_demos/ios_calendar_demo/pubspec.yaml b/native_interop_demos/ios_calendar_demo/pubspec.yaml new file mode 100644 index 0000000..4f02e47 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/pubspec.yaml @@ -0,0 +1,24 @@ +name: ios_calendar_demo +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0+1 + +environment: + sdk: ^3.13.0-201.0.dev + +dependencies: + ffi: ^2.2.0 + flutter: + sdk: flutter + objective_c: ^9.4.1 + permission_handler: ^12.0.3 + swiftgen: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + ffigen: ^20.1.1 + +flutter: + uses-material-design: true diff --git a/native_interop_demos/ios_calendar_demo/tool/generate_code.dart b/native_interop_demos/ios_calendar_demo/tool/generate_code.dart new file mode 100644 index 0000000..f8385bb --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/tool/generate_code.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:ffigen/ffigen.dart'; + +void main() { + print('Generating EventKit bindings directly from Objective-C headers...'); + final generator = FfiGenerator( + headers: Headers( + entryPoints: [ + Uri.file( + '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/EventKit.framework/Headers/EventKit.h', + ), + ], + ), + objectiveC: ObjectiveC( + interfaces: Interfaces.includeSet({ + 'EKEventStore', + 'EKEvent', + 'EKCalendar', + 'EKCalendarItem', + 'EKObject', + }), + ), + enums: Enums.includeSet({'EKEntityType'}), + output: Output( + dartFile: Uri.file('lib/eventkit_bindings.dart'), + preamble: ''' +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Auto-generated by swiftgen +// coverage:ignore-file +''', + ), + ); + + generator.generate(); + + // Clean up old swiftgen wrapper files since they are no longer needed + final swiftFile = File('ios/Runner/eventkit_wrapper.swift'); + if (swiftFile.existsSync()) swiftFile.deleteSync(); + + final objcFileOld = File('ios/Runner/eventkit_wrapper.m'); + if (objcFileOld.existsSync()) objcFileOld.deleteSync(); + + print('Done.'); +} diff --git a/native_interop_demos/ios_calendar_demo/tool/test_decl.dart b/native_interop_demos/ios_calendar_demo/tool/test_decl.dart new file mode 100644 index 0000000..afa05f6 --- /dev/null +++ b/native_interop_demos/ios_calendar_demo/tool/test_decl.dart @@ -0,0 +1,17 @@ +import 'package:ios_calendar_demo/eventkit_bindings.dart'; +import 'package:objective_c/objective_c.dart' as objc; + +void main() { + final store = EKEventStore(); + final startDate = objc.NSDate.date(); + final endDate = objc.NSDate.dateWithTimeIntervalSinceNow(30.0 * 24 * 60 * 60); + final predicate = store.predicateForEventsWithStartDate(startDate, endDate: endDate); + final eventsArray = store.eventsMatchingPredicate(predicate); + + final count = eventsArray.count; + for (int i = 0; i < count; i++) { + final obj = eventsArray.objectAtIndex(i); + final event = EKEvent.as(obj); + final int result = event.startDate.compare(event.endDate).value; + } +} diff --git a/native_interop_demos/jnigen_db_native_interop/.gitignore b/native_interop_demos/jnigen_db_native_interop/.gitignore new file mode 100644 index 0000000..58ee391 --- /dev/null +++ b/native_interop_demos/jnigen_db_native_interop/.gitignore @@ -0,0 +1,5 @@ +/mvn_java +/.dart_tool +.DS_Store +/.idea +/build diff --git a/native_interop_demos/jnigen_db_native_interop/LICENSE b/native_interop_demos/jnigen_db_native_interop/LICENSE new file mode 100644 index 0000000..9748873 --- /dev/null +++ b/native_interop_demos/jnigen_db_native_interop/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025, James Williams + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/native_interop_demos/jnigen_db_native_interop/README.md b/native_interop_demos/jnigen_db_native_interop/README.md new file mode 100644 index 0000000..ea7b30a --- /dev/null +++ b/native_interop_demos/jnigen_db_native_interop/README.md @@ -0,0 +1,116 @@ +# jni_leveldb + +A sample command-line application demonstrating how to create an idiomatic Dart wrapper around a Java implementation of LevelDB, a fast key-value storage library. This project showcases the evolution of a LevelDB wrapper from raw JNI bindings to a safe, clean, and Dart-native API using `jnigen`. + +## Prerequisites + +- Java Development Kit (JDK) version 1.8 or later must be installed, and the `JAVA_HOME` environment variable must be set to its location. + +## Setup and Running + +1. **Generate Bindings and Build JNI code:** + + Run `jnigen` to download the required Java libraries, generate Dart bindings, and build the JNI glue code. + + ```bash + dart run jni:setup + dart run jnigen:setup + dart run jnigen --config jnigen.yaml + ``` + +2. **Run the application:** + + ```bash + dart run bin/jni_leveldb.dart + ``` + +## From Raw Bindings to an Idiomatic Dart API + +The initial output of `jnigen` provides a low-level, direct mapping of the Java API. Using this directly in application code can be verbose, unsafe, and unidiomatic. This project demonstrates how to build a better wrapper by addressing common pitfalls. + +### 1. Resource Management + +JNI objects are handles to resources in the JVM. Failing to release them causes memory leaks. + +**The Problem:** Forgetting to call `release()` on JNI objects. + +**The Solution:** The best practice is to use the `using(Arena arena)` block, which automatically manages releasing all objects allocated within it, making your code safer and cleaner. For objects that live longer, you must manually call `release()`. + +*Example from the wrapper:* +```dart +void putBytes(Uint8List key, Uint8List value) { + using((arena) { + final jKey = JByteArray.from(key)..releasedBy(arena); + final jValue = JByteArray.from(value)..releasedBy(arena); + _db.put(jKey, jValue); + }); +} +``` + +### 2. Idiomatic API Design and Type Handling + +Raw bindings expose Java's conventions and require manual, repetitive type conversions. A wrapper class should expose a clean, Dart-like API. + +**The Problem:** Java method names (`createIfMissing$1`) and types (`JString`, `JByteArray`) are exposed directly to the application code. + +**The Solution:** Create a wrapper class that exposes methods with named parameters and standard Dart types (`String`, `Uint8List`), handling all the JNI conversions internally. + +*The Improved API:* +```dart +static LevelDB open(String path, {bool createIfMissing = true}) { ... } +void put(String key, String value) { ... } +String? get(String key) { ... } +``` + +This allows for clean, simple iteration in the application code: +```dart +for (var entry in db.entries) { + print('${entry.key}, ${entry.value}'); +} +``` + +### 3. JVM Initialization + +The JVM is a process-level resource and should be initialized only once when the application starts. + +**The Problem:** Calling `Jni.spawn()` inside library code. + +**The Solution:** `Jni.spawn()` belongs in a locatio where it will be called once, like your application's `main()` function, not in the library. In this example, the library code should assume the JVM is already running. + +*Correct Usage in `bin/jni_leveldb.dart`:* +```dart +void main(List arguments) { + // ... find JARs ... + Jni.spawn(classPath: jars); // Spawn the JVM once. + db(); // Run the application logic. +} +``` + +### The Final Result + +By applying these principles, the application logic becomes simple, readable, and free of JNI-specific details. + +*Final Application Code:* +```dart +import 'package:jni_leveldb/src/leveldb.dart'; + +void db() { + final db = LevelDB.open('example.db'); + try { + db.put('Akron', 'Ohio'); + db.put('Tampa', 'Florida'); + db.put('Cleveland', 'Ohio'); + + print('Tampa is in ${db.get('Tampa')}'); + + db.delete('Akron'); + + print('\nEntries in database:'); + for (var entry in db.entries) { + print('${entry.key}, ${entry.value}'); + } + } finally { + db.close(); + } +} +``` \ No newline at end of file diff --git a/native_interop_demos/jnigen_db_native_interop/bin/jni_leveldb.dart b/native_interop_demos/jnigen_db_native_interop/bin/jni_leveldb.dart new file mode 100644 index 0000000..fd0bf38 --- /dev/null +++ b/native_interop_demos/jnigen_db_native_interop/bin/jni_leveldb.dart @@ -0,0 +1,30 @@ +import 'package:jni_leveldb/jni_leveldb.dart' as jni_leveldb; +import 'dart:io'; +import 'package:jni/jni.dart'; + +import 'package:path/path.dart'; + +const jarError = ''; + +void main(List arguments) { + +const jarDir = './mvn_jar/'; + List jars; + try { + jars = Directory(jarDir) + .listSync() + .map((e) => e.path) + .where((path) => path.endsWith('.jar')) + .toList(); + } on OSError catch (_) { + stderr.writeln(jarError); + return; + } + if (jars.isEmpty) { + stderr.writeln(jarError); + return; + } + Jni.spawn(classPath: jars); + + jni_leveldb.db(); +} diff --git a/native_interop_demos/jnigen_db_native_interop/lib/jni_leveldb.dart b/native_interop_demos/jnigen_db_native_interop/lib/jni_leveldb.dart new file mode 100644 index 0000000..a00fef8 --- /dev/null +++ b/native_interop_demos/jnigen_db_native_interop/lib/jni_leveldb.dart @@ -0,0 +1,22 @@ +import 'package:jni_leveldb/src/leveldb.dart'; + +void db() { + final db = LevelDB.open('example.db'); + try { + db.put('Akron', 'Ohio'); + db.put('Tampa', 'Florida'); + db.put('Cleveland', 'Ohio'); + db.put('Sunnyvale', 'California'); + + print('Tampa is in ${db.get('Tampa')}'); + + db.delete('Akron'); + + print('\nEntries in database:'); + for (var entry in db.entries) { + print('${entry.key}, ${entry.value}'); + } + } finally { + db.close(); + } +} \ No newline at end of file diff --git a/native_interop_demos/jnigen_db_native_interop/lib/src/leveldb.dart b/native_interop_demos/jnigen_db_native_interop/lib/src/leveldb.dart new file mode 100644 index 0000000..5fe4a34 --- /dev/null +++ b/native_interop_demos/jnigen_db_native_interop/lib/src/leveldb.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:jni/jni.dart'; +import 'package:jni_leveldb/gen/leveldb/java/io/File.dart' as java; +import 'package:jni_leveldb/gen/leveldb/org/iq80/leveldb/DB.dart'; +import 'package:jni_leveldb/gen/leveldb/org/iq80/leveldb/Options.dart'; +import 'package:jni_leveldb/gen/leveldb/org/iq80/leveldb/impl/Iq80DBFactory.dart'; +import 'package:jni_leveldb/gen/leveldb/org/iq80/leveldb/impl/SeekingIteratorAdapter.dart'; + +class LevelDB { + final DB _db; + + LevelDB._(this._db); + + static LevelDB open(String path, {bool createIfMissing = true}) { + final options = Options()..createIfMissing$1(createIfMissing); + final file = java.File(path.toJString()); + final db = Iq80DBFactory.factory!.open(file, options); + if (db == null) { + throw Exception('Failed to open database at $path'); + } + return LevelDB._(db); + } + + void put(String key, String value) { + putBytes(utf8.encode(key), utf8.encode(value)); + } + + void putBytes(Uint8List key, Uint8List value) { + using((arena) { + final jKey = JByteArray.from(key)..releasedBy(arena); + final jValue = JByteArray.from(value)..releasedBy(arena); + _db.put(jKey, jValue); + }); + } + + String? get(String key) { + final value = getBytes(utf8.encode(key)); + if (value == null) { + return null; + } + return utf8.decode(value); + } + + Uint8List? getBytes(Uint8List key) { + return using((arena) { + final jKey = JByteArray.from(key)..releasedBy(arena); + final value = _db.get(jKey); + if (value == null) { + return null; + } + final bytes = value.toList(); + value.release(); + return Uint8List.fromList(bytes); + }); + } + + void delete(String key) { + deleteBytes(utf8.encode(key)); + } + + void deleteBytes(Uint8List key) { + using((arena) { + final jKey = JByteArray.from(key)..releasedBy(arena); + _db.delete(jKey); + }); + } + + void close() { + _db.release(); + } + + Iterable> get entries sync* { + final iterator = _db.iterator()?.as(SeekingIteratorAdapter.type); + if (iterator == null) return; + try { + iterator.seekToFirst(); + while (iterator.hasNext()) { + final entry = iterator.next(); + if (entry == null) continue; + + final keyBytes = entry.getKey(); + final valueBytes = entry.getValue(); + + if (keyBytes == null || valueBytes == null) { + keyBytes?.release(); + valueBytes?.release(); + entry.release(); + continue; + } + + final key = utf8.decode(keyBytes.toList()); + final value = utf8.decode(valueBytes.toList()); + + keyBytes.release(); + valueBytes.release(); + entry.release(); + + yield MapEntry(key, value); + } + } finally { + iterator.release(); + } + } +} diff --git a/native_interop_demos/jnigen_db_native_interop/pubspec.yaml b/native_interop_demos/jnigen_db_native_interop/pubspec.yaml new file mode 100644 index 0000000..9467cff --- /dev/null +++ b/native_interop_demos/jnigen_db_native_interop/pubspec.yaml @@ -0,0 +1,18 @@ +name: jni_leveldb +description: A sample command-line application. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +publish_to: none + +environment: + sdk: ^3.6.2 + +# Add regular dependencies here. +dependencies: + jni: ^0.15.2 + +dev_dependencies: + jnigen: ^0.15.0 + lints: ^5.0.0 + test: ^1.24.0 diff --git a/native_interop_demos/jnigen_db_native_interop/tool/jnigen.dart b/native_interop_demos/jnigen_db_native_interop/tool/jnigen.dart new file mode 100644 index 0000000..2dbdf8b --- /dev/null +++ b/native_interop_demos/jnigen_db_native_interop/tool/jnigen.dart @@ -0,0 +1,27 @@ +import 'dart:io'; + +import 'package:jnigen/jnigen.dart'; + +void main(List args) { + final packageRoot = Platform.script.resolve('../'); + generateJniBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: packageRoot.resolve('lib/gen/leveldb/'), + structure: OutputStructure.packageStructure, + ), + ), + mavenDownloads: + MavenDownloads(sourceDeps: ['org.iq80.leveldb:leveldb:0.12']), + classes: [ + 'org.iq80.leveldb.DB', + 'org.iq80.leveldb.Options', + 'org.iq80.leveldb.DBIterator', + 'org.iq80.leveldb.impl.Iq80DBFactory', + 'org.iq80.leveldb.impl.SeekingIteratorAdapter', + 'java.io.File' + ], + ), + ); +} diff --git a/native_interop_demos/permissions_plugin/.gitignore b/native_interop_demos/permissions_plugin/.gitignore new file mode 100644 index 0000000..b9d7f25 --- /dev/null +++ b/native_interop_demos/permissions_plugin/.gitignore @@ -0,0 +1,33 @@ +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ diff --git a/native_interop_demos/permissions_plugin/.metadata b/native_interop_demos/permissions_plugin/.metadata new file mode 100644 index 0000000..74605d9 --- /dev/null +++ b/native_interop_demos/permissions_plugin/.metadata @@ -0,0 +1,30 @@ +# 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: "67323de285b00232883f53b84095eb72be97d35c" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 67323de285b00232883f53b84095eb72be97d35c + base_revision: 67323de285b00232883f53b84095eb72be97d35c + - platform: android + create_revision: 67323de285b00232883f53b84095eb72be97d35c + base_revision: 67323de285b00232883f53b84095eb72be97d35c + + # 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' diff --git a/native_interop_demos/permissions_plugin/CHANGELOG.md b/native_interop_demos/permissions_plugin/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/native_interop_demos/permissions_plugin/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/native_interop_demos/permissions_plugin/LICENSE b/native_interop_demos/permissions_plugin/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/native_interop_demos/permissions_plugin/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/native_interop_demos/permissions_plugin/README.md b/native_interop_demos/permissions_plugin/README.md new file mode 100644 index 0000000..9890a6d --- /dev/null +++ b/native_interop_demos/permissions_plugin/README.md @@ -0,0 +1,11 @@ +# permissions_plugin + +Demonstrates how to use `jnigen` and native interop to create a plugin to query and request Android permissions. + +## To build + +```sh +flutter build apk +dart tool/jnigen.dart +flutter run +``` diff --git a/native_interop_demos/permissions_plugin/analysis_options.yaml b/native_interop_demos/permissions_plugin/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/native_interop_demos/permissions_plugin/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/native_interop_demos/permissions_plugin/android/.gitignore b/native_interop_demos/permissions_plugin/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/native_interop_demos/permissions_plugin/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/native_interop_demos/permissions_plugin/android/build.gradle b/native_interop_demos/permissions_plugin/android/build.gradle new file mode 100644 index 0000000..be4d416 --- /dev/null +++ b/native_interop_demos/permissions_plugin/android/build.gradle @@ -0,0 +1,68 @@ +group = "com.example.permissions_plugin" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.2.20" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.11.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "com.example.permissions_plugin" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 24 + } + + dependencies { + def activity_version = "1.12.4" + implementation("androidx.activity:activity-ktx:$activity_version") + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/native_interop_demos/permissions_plugin/android/settings.gradle b/native_interop_demos/permissions_plugin/android/settings.gradle new file mode 100644 index 0000000..7e2783f --- /dev/null +++ b/native_interop_demos/permissions_plugin/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'permissions_plugin' diff --git a/native_interop_demos/permissions_plugin/android/src/main/AndroidManifest.xml b/native_interop_demos/permissions_plugin/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8ec69c2 --- /dev/null +++ b/native_interop_demos/permissions_plugin/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/native_interop_demos/permissions_plugin/android/src/main/kotlin/com/example/permissions_plugin/PermissionsPlugin.kt b/native_interop_demos/permissions_plugin/android/src/main/kotlin/com/example/permissions_plugin/PermissionsPlugin.kt new file mode 100644 index 0000000..0ed7e0b --- /dev/null +++ b/native_interop_demos/permissions_plugin/android/src/main/kotlin/com/example/permissions_plugin/PermissionsPlugin.kt @@ -0,0 +1,38 @@ +package com.example.permissions_plugin + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** PermissionsPlugin */ +class PermissionsPlugin : + FlutterPlugin, + MethodCallHandler { + // The MethodChannel that will the communication between Flutter and native Android + // + // This local reference serves to register the plugin with the Flutter Engine and unregister it + // when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "permissions_plugin") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall( + call: MethodCall, + result: Result + ) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/native_interop_demos/permissions_plugin/android/src/test/kotlin/com/example/permissions_plugin/PermissionsPluginTest.kt b/native_interop_demos/permissions_plugin/android/src/test/kotlin/com/example/permissions_plugin/PermissionsPluginTest.kt new file mode 100644 index 0000000..3ae5d0d --- /dev/null +++ b/native_interop_demos/permissions_plugin/android/src/test/kotlin/com/example/permissions_plugin/PermissionsPluginTest.kt @@ -0,0 +1,27 @@ +package com.example.permissions_plugin + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.mockito.Mockito +import kotlin.test.Test + +/* + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +internal class PermissionsPluginTest { + @Test + fun onMethodCall_getPlatformVersion_returnsExpectedValue() { + val plugin = PermissionsPlugin() + + val call = MethodCall("getPlatformVersion", null) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) + } +} diff --git a/native_interop_demos/permissions_plugin/example/.gitignore b/native_interop_demos/permissions_plugin/example/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/native_interop_demos/permissions_plugin/example/README.md b/native_interop_demos/permissions_plugin/example/README.md new file mode 100644 index 0000000..c8be267 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/README.md @@ -0,0 +1,16 @@ +# permissions_plugin_example + +Demonstrates how to use the permissions_plugin plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/native_interop_demos/permissions_plugin/example/analysis_options.yaml b/native_interop_demos/permissions_plugin/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/native_interop_demos/permissions_plugin/example/android/.gitignore b/native_interop_demos/permissions_plugin/example/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/native_interop_demos/permissions_plugin/example/android/app/build.gradle.kts b/native_interop_demos/permissions_plugin/example/android/app/build.gradle.kts new file mode 100644 index 0000000..3ce960d --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/app/build.gradle.kts @@ -0,0 +1,49 @@ +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") +} + +android { + namespace = "com.example.permissions_plugin_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.permissions_plugin_example" + // 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 + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } + + kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +} + + + +flutter { + source = "../.." +} diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/debug/AndroidManifest.xml b/native_interop_demos/permissions_plugin/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/AndroidManifest.xml b/native_interop_demos/permissions_plugin/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4ca54f9 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/kotlin/com/example/permissions_plugin_example/MainActivity.kt b/native_interop_demos/permissions_plugin/example/android/app/src/main/kotlin/com/example/permissions_plugin_example/MainActivity.kt new file mode 100644 index 0000000..e8489bd --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/app/src/main/kotlin/com/example/permissions_plugin_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.permissions_plugin_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/res/drawable-v21/launch_background.xml b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/res/drawable/launch_background.xml b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/res/values-night/styles.xml b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/main/res/values/styles.xml b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/native_interop_demos/permissions_plugin/example/android/app/src/profile/AndroidManifest.xml b/native_interop_demos/permissions_plugin/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/native_interop_demos/permissions_plugin/example/android/build.gradle.kts b/native_interop_demos/permissions_plugin/example/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/build.gradle.kts @@ -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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/native_interop_demos/permissions_plugin/example/android/build/reports/problems/problems-report.html b/native_interop_demos/permissions_plugin/example/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..4a82fb8 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/native_interop_demos/permissions_plugin/example/android/gradle.properties b/native_interop_demos/permissions_plugin/example/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/native_interop_demos/permissions_plugin/example/android/gradle/wrapper/gradle-wrapper.properties b/native_interop_demos/permissions_plugin/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/gradle/wrapper/gradle-wrapper.properties @@ -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-all.zip diff --git a/native_interop_demos/permissions_plugin/example/android/settings.gradle.kts b/native_interop_demos/permissions_plugin/example/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/android/settings.gradle.kts @@ -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.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/native_interop_demos/permissions_plugin/example/integration_test/plugin_integration_test.dart b/native_interop_demos/permissions_plugin/example/integration_test/plugin_integration_test.dart new file mode 100644 index 0000000..dbe595d --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/integration_test/plugin_integration_test.dart @@ -0,0 +1,25 @@ +// This is a basic Flutter integration test. +// +// Since integration tests run in a full Flutter application, they can interact +// with the host side of a plugin implementation, unlike Dart unit tests. +// +// For more information about Flutter integration tests, please see +// https://flutter.dev/to/integration-testing + + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:permissions_plugin/permissions_plugin.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getPlatformVersion test', (WidgetTester tester) async { + final PermissionsPlugin plugin = PermissionsPlugin(); + final String? version = await plugin.getPlatformVersion(); + // The version string depends on the host platform running the test, so + // just assert that some non-empty string is returned. + expect(version?.isNotEmpty, true); + }); +} diff --git a/native_interop_demos/permissions_plugin/example/lib/main.dart b/native_interop_demos/permissions_plugin/example/lib/main.dart new file mode 100644 index 0000000..f5a4e3d --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/lib/main.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:permissions_plugin/gen/android.g.dart'; +import 'package:permissions_plugin/permissions_plugin.dart'; +import 'package:jni/jni.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + final _permissionsPlugin = PermissionsPlugin(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + platformVersion = + await _permissionsPlugin.getPlatformVersion() ?? + 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Plugin example app')), + body: Center( + child: Column( + children: [ + Text('Running on: $_platformVersion\n'), + FilledButton( + child: Text("Request Camera Permissions"), + onPressed: () { +_permissionsPlugin.checkAndRequestPermission( + Jni.androidApplicationContext, + "android.permission.CAMERA", + () {}, + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/native_interop_demos/permissions_plugin/example/pubspec.yaml b/native_interop_demos/permissions_plugin/example/pubspec.yaml new file mode 100644 index 0000000..315b49e --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/pubspec.yaml @@ -0,0 +1,85 @@ +name: permissions_plugin_example +description: "Demonstrates how to use the permissions_plugin plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ^3.10.8 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + permissions_plugin: + # When depending on this package from a real application you should use: + # permissions_plugin: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/native_interop_demos/permissions_plugin/example/test/widget_test.dart b/native_interop_demos/permissions_plugin/example/test/widget_test.dart new file mode 100644 index 0000000..570eca9 --- /dev/null +++ b/native_interop_demos/permissions_plugin/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:permissions_plugin_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/native_interop_demos/permissions_plugin/lib/gen/android.g.dart b/native_interop_demos/permissions_plugin/lib/gen/android.g.dart new file mode 100644 index 0000000..214b41b --- /dev/null +++ b/native_interop_demos/permissions_plugin/lib/gen/android.g.dart @@ -0,0 +1,33574 @@ +// AUTO GENERATED BY JNIGEN 0.15.0. DO NOT EDIT! + +// ignore_for_file: annotate_overrides +// ignore_for_file: argument_type_not_assignable +// ignore_for_file: camel_case_extensions +// ignore_for_file: camel_case_types +// ignore_for_file: constant_identifier_names +// ignore_for_file: comment_references +// ignore_for_file: doc_directive_unknown +// ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter +// ignore_for_file: invalid_internal_annotation +// ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: library_prefixes +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: no_leading_underscores_for_library_prefixes +// ignore_for_file: no_leading_underscores_for_local_identifiers +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: only_throw_errors +// ignore_for_file: overridden_fields +// ignore_for_file: prefer_double_quotes +// ignore_for_file: unintended_html_in_doc_comment +// ignore_for_file: unnecessary_cast +// ignore_for_file: unnecessary_non_null_assertion +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: unused_element +// ignore_for_file: unused_field +// ignore_for_file: unused_import +// ignore_for_file: unused_local_variable +// ignore_for_file: unused_shown_name +// ignore_for_file: use_super_parameters + +import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; + +import 'package:jni/_internal.dart' as jni$_; +import 'package:jni/jni.dart' as jni$_; + +/// from: `android.app.Application$ActivityLifecycleCallbacks` +class Application$ActivityLifecycleCallbacks extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Application$ActivityLifecycleCallbacks.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/app/Application$ActivityLifecycleCallbacks', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType + nullableType = $Application$ActivityLifecycleCallbacks$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $Application$ActivityLifecycleCallbacks$Type$(); + static final _id_onActivityCreated = _class.instanceMethodId( + r'onActivityCreated', + r'(Landroid/app/Activity;Landroid/os/Bundle;)V', + ); + + static final _onActivityCreated = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onActivityCreated(android.app.Activity activity, android.os.Bundle bundle)` + void onActivityCreated(jni$_.JObject? activity, jni$_.JObject? bundle) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _onActivityCreated( + reference.pointer, + _id_onActivityCreated as jni$_.JMethodIDPtr, + _$activity.pointer, + _$bundle.pointer, + ).check(); + } + + static final _id_onActivityDestroyed = _class.instanceMethodId( + r'onActivityDestroyed', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityDestroyed = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onActivityDestroyed(android.app.Activity activity)` + void onActivityDestroyed(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityDestroyed( + reference.pointer, + _id_onActivityDestroyed as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPaused = _class.instanceMethodId( + r'onActivityPaused', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPaused = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onActivityPaused(android.app.Activity activity)` + void onActivityPaused(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPaused( + reference.pointer, + _id_onActivityPaused as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPostCreated = _class.instanceMethodId( + r'onActivityPostCreated', + r'(Landroid/app/Activity;Landroid/os/Bundle;)V', + ); + + static final _onActivityPostCreated = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPostCreated(android.app.Activity activity, android.os.Bundle bundle)` + void onActivityPostCreated(jni$_.JObject? activity, jni$_.JObject? bundle) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _onActivityPostCreated( + reference.pointer, + _id_onActivityPostCreated as jni$_.JMethodIDPtr, + _$activity.pointer, + _$bundle.pointer, + ).check(); + } + + static final _id_onActivityPostDestroyed = _class.instanceMethodId( + r'onActivityPostDestroyed', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPostDestroyed = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPostDestroyed(android.app.Activity activity)` + void onActivityPostDestroyed(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPostDestroyed( + reference.pointer, + _id_onActivityPostDestroyed as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPostPaused = _class.instanceMethodId( + r'onActivityPostPaused', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPostPaused = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPostPaused(android.app.Activity activity)` + void onActivityPostPaused(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPostPaused( + reference.pointer, + _id_onActivityPostPaused as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPostResumed = _class.instanceMethodId( + r'onActivityPostResumed', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPostResumed = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPostResumed(android.app.Activity activity)` + void onActivityPostResumed(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPostResumed( + reference.pointer, + _id_onActivityPostResumed as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPostSaveInstanceState = _class.instanceMethodId( + r'onActivityPostSaveInstanceState', + r'(Landroid/app/Activity;Landroid/os/Bundle;)V', + ); + + static final _onActivityPostSaveInstanceState = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPostSaveInstanceState(android.app.Activity activity, android.os.Bundle bundle)` + void onActivityPostSaveInstanceState( + jni$_.JObject? activity, + jni$_.JObject? bundle, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _onActivityPostSaveInstanceState( + reference.pointer, + _id_onActivityPostSaveInstanceState as jni$_.JMethodIDPtr, + _$activity.pointer, + _$bundle.pointer, + ).check(); + } + + static final _id_onActivityPostStarted = _class.instanceMethodId( + r'onActivityPostStarted', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPostStarted = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPostStarted(android.app.Activity activity)` + void onActivityPostStarted(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPostStarted( + reference.pointer, + _id_onActivityPostStarted as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPostStopped = _class.instanceMethodId( + r'onActivityPostStopped', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPostStopped = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPostStopped(android.app.Activity activity)` + void onActivityPostStopped(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPostStopped( + reference.pointer, + _id_onActivityPostStopped as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPreCreated = _class.instanceMethodId( + r'onActivityPreCreated', + r'(Landroid/app/Activity;Landroid/os/Bundle;)V', + ); + + static final _onActivityPreCreated = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPreCreated(android.app.Activity activity, android.os.Bundle bundle)` + void onActivityPreCreated(jni$_.JObject? activity, jni$_.JObject? bundle) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _onActivityPreCreated( + reference.pointer, + _id_onActivityPreCreated as jni$_.JMethodIDPtr, + _$activity.pointer, + _$bundle.pointer, + ).check(); + } + + static final _id_onActivityPreDestroyed = _class.instanceMethodId( + r'onActivityPreDestroyed', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPreDestroyed = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPreDestroyed(android.app.Activity activity)` + void onActivityPreDestroyed(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPreDestroyed( + reference.pointer, + _id_onActivityPreDestroyed as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPrePaused = _class.instanceMethodId( + r'onActivityPrePaused', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPrePaused = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPrePaused(android.app.Activity activity)` + void onActivityPrePaused(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPrePaused( + reference.pointer, + _id_onActivityPrePaused as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPreResumed = _class.instanceMethodId( + r'onActivityPreResumed', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPreResumed = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPreResumed(android.app.Activity activity)` + void onActivityPreResumed(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPreResumed( + reference.pointer, + _id_onActivityPreResumed as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPreSaveInstanceState = _class.instanceMethodId( + r'onActivityPreSaveInstanceState', + r'(Landroid/app/Activity;Landroid/os/Bundle;)V', + ); + + static final _onActivityPreSaveInstanceState = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPreSaveInstanceState(android.app.Activity activity, android.os.Bundle bundle)` + void onActivityPreSaveInstanceState( + jni$_.JObject? activity, + jni$_.JObject? bundle, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _onActivityPreSaveInstanceState( + reference.pointer, + _id_onActivityPreSaveInstanceState as jni$_.JMethodIDPtr, + _$activity.pointer, + _$bundle.pointer, + ).check(); + } + + static final _id_onActivityPreStarted = _class.instanceMethodId( + r'onActivityPreStarted', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPreStarted = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPreStarted(android.app.Activity activity)` + void onActivityPreStarted(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPreStarted( + reference.pointer, + _id_onActivityPreStarted as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityPreStopped = _class.instanceMethodId( + r'onActivityPreStopped', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityPreStopped = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onActivityPreStopped(android.app.Activity activity)` + void onActivityPreStopped(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityPreStopped( + reference.pointer, + _id_onActivityPreStopped as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityResumed = _class.instanceMethodId( + r'onActivityResumed', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityResumed = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onActivityResumed(android.app.Activity activity)` + void onActivityResumed(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityResumed( + reference.pointer, + _id_onActivityResumed as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivitySaveInstanceState = _class.instanceMethodId( + r'onActivitySaveInstanceState', + r'(Landroid/app/Activity;Landroid/os/Bundle;)V', + ); + + static final _onActivitySaveInstanceState = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onActivitySaveInstanceState(android.app.Activity activity, android.os.Bundle bundle)` + void onActivitySaveInstanceState( + jni$_.JObject? activity, + jni$_.JObject? bundle, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _onActivitySaveInstanceState( + reference.pointer, + _id_onActivitySaveInstanceState as jni$_.JMethodIDPtr, + _$activity.pointer, + _$bundle.pointer, + ).check(); + } + + static final _id_onActivityStarted = _class.instanceMethodId( + r'onActivityStarted', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityStarted = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onActivityStarted(android.app.Activity activity)` + void onActivityStarted(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityStarted( + reference.pointer, + _id_onActivityStarted as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_onActivityStopped = _class.instanceMethodId( + r'onActivityStopped', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityStopped = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onActivityStopped(android.app.Activity activity)` + void onActivityStopped(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityStopped( + reference.pointer, + _id_onActivityStopped as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map + _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses(0, descriptor.address, args.address), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == + r'onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V') { + _$impls[$p]!.onActivityCreated( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + $a![1]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityDestroyed(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityDestroyed( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPaused(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPaused( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == + r'onActivityPostCreated(Landroid/app/Activity;Landroid/os/Bundle;)V') { + _$impls[$p]!.onActivityPostCreated( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + $a![1]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPostDestroyed(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPostDestroyed( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPostPaused(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPostPaused( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPostResumed(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPostResumed( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == + r'onActivityPostSaveInstanceState(Landroid/app/Activity;Landroid/os/Bundle;)V') { + _$impls[$p]!.onActivityPostSaveInstanceState( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + $a![1]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPostStarted(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPostStarted( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPostStopped(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPostStopped( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == + r'onActivityPreCreated(Landroid/app/Activity;Landroid/os/Bundle;)V') { + _$impls[$p]!.onActivityPreCreated( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + $a![1]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPreDestroyed(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPreDestroyed( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPrePaused(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPrePaused( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPreResumed(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPreResumed( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == + r'onActivityPreSaveInstanceState(Landroid/app/Activity;Landroid/os/Bundle;)V') { + _$impls[$p]!.onActivityPreSaveInstanceState( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + $a![1]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPreStarted(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPreStarted( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityPreStopped(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityPreStopped( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityResumed(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityResumed( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == + r'onActivitySaveInstanceState(Landroid/app/Activity;Landroid/os/Bundle;)V') { + _$impls[$p]!.onActivitySaveInstanceState( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + $a![1]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityStarted(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityStarted( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + if ($d == r'onActivityStopped(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityStopped( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $Application$ActivityLifecycleCallbacks $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'android.app.Application$ActivityLifecycleCallbacks', + $p, + _$invokePointer, + [ + if ($impl.onActivityCreated$async) + r'onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V', + if ($impl.onActivityDestroyed$async) + r'onActivityDestroyed(Landroid/app/Activity;)V', + if ($impl.onActivityPaused$async) + r'onActivityPaused(Landroid/app/Activity;)V', + if ($impl.onActivityPostCreated$async) + r'onActivityPostCreated(Landroid/app/Activity;Landroid/os/Bundle;)V', + if ($impl.onActivityPostDestroyed$async) + r'onActivityPostDestroyed(Landroid/app/Activity;)V', + if ($impl.onActivityPostPaused$async) + r'onActivityPostPaused(Landroid/app/Activity;)V', + if ($impl.onActivityPostResumed$async) + r'onActivityPostResumed(Landroid/app/Activity;)V', + if ($impl.onActivityPostSaveInstanceState$async) + r'onActivityPostSaveInstanceState(Landroid/app/Activity;Landroid/os/Bundle;)V', + if ($impl.onActivityPostStarted$async) + r'onActivityPostStarted(Landroid/app/Activity;)V', + if ($impl.onActivityPostStopped$async) + r'onActivityPostStopped(Landroid/app/Activity;)V', + if ($impl.onActivityPreCreated$async) + r'onActivityPreCreated(Landroid/app/Activity;Landroid/os/Bundle;)V', + if ($impl.onActivityPreDestroyed$async) + r'onActivityPreDestroyed(Landroid/app/Activity;)V', + if ($impl.onActivityPrePaused$async) + r'onActivityPrePaused(Landroid/app/Activity;)V', + if ($impl.onActivityPreResumed$async) + r'onActivityPreResumed(Landroid/app/Activity;)V', + if ($impl.onActivityPreSaveInstanceState$async) + r'onActivityPreSaveInstanceState(Landroid/app/Activity;Landroid/os/Bundle;)V', + if ($impl.onActivityPreStarted$async) + r'onActivityPreStarted(Landroid/app/Activity;)V', + if ($impl.onActivityPreStopped$async) + r'onActivityPreStopped(Landroid/app/Activity;)V', + if ($impl.onActivityResumed$async) + r'onActivityResumed(Landroid/app/Activity;)V', + if ($impl.onActivitySaveInstanceState$async) + r'onActivitySaveInstanceState(Landroid/app/Activity;Landroid/os/Bundle;)V', + if ($impl.onActivityStarted$async) + r'onActivityStarted(Landroid/app/Activity;)V', + if ($impl.onActivityStopped$async) + r'onActivityStopped(Landroid/app/Activity;)V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory Application$ActivityLifecycleCallbacks.implement( + $Application$ActivityLifecycleCallbacks $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return Application$ActivityLifecycleCallbacks.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $Application$ActivityLifecycleCallbacks { + factory $Application$ActivityLifecycleCallbacks({ + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityCreated, + bool onActivityCreated$async, + required void Function(jni$_.JObject? activity) onActivityDestroyed, + bool onActivityDestroyed$async, + required void Function(jni$_.JObject? activity) onActivityPaused, + bool onActivityPaused$async, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityPostCreated, + bool onActivityPostCreated$async, + required void Function(jni$_.JObject? activity) onActivityPostDestroyed, + bool onActivityPostDestroyed$async, + required void Function(jni$_.JObject? activity) onActivityPostPaused, + bool onActivityPostPaused$async, + required void Function(jni$_.JObject? activity) onActivityPostResumed, + bool onActivityPostResumed$async, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityPostSaveInstanceState, + bool onActivityPostSaveInstanceState$async, + required void Function(jni$_.JObject? activity) onActivityPostStarted, + bool onActivityPostStarted$async, + required void Function(jni$_.JObject? activity) onActivityPostStopped, + bool onActivityPostStopped$async, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityPreCreated, + bool onActivityPreCreated$async, + required void Function(jni$_.JObject? activity) onActivityPreDestroyed, + bool onActivityPreDestroyed$async, + required void Function(jni$_.JObject? activity) onActivityPrePaused, + bool onActivityPrePaused$async, + required void Function(jni$_.JObject? activity) onActivityPreResumed, + bool onActivityPreResumed$async, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityPreSaveInstanceState, + bool onActivityPreSaveInstanceState$async, + required void Function(jni$_.JObject? activity) onActivityPreStarted, + bool onActivityPreStarted$async, + required void Function(jni$_.JObject? activity) onActivityPreStopped, + bool onActivityPreStopped$async, + required void Function(jni$_.JObject? activity) onActivityResumed, + bool onActivityResumed$async, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivitySaveInstanceState, + bool onActivitySaveInstanceState$async, + required void Function(jni$_.JObject? activity) onActivityStarted, + bool onActivityStarted$async, + required void Function(jni$_.JObject? activity) onActivityStopped, + bool onActivityStopped$async, + }) = _$Application$ActivityLifecycleCallbacks; + + void onActivityCreated(jni$_.JObject? activity, jni$_.JObject? bundle); + bool get onActivityCreated$async => false; + void onActivityDestroyed(jni$_.JObject? activity); + bool get onActivityDestroyed$async => false; + void onActivityPaused(jni$_.JObject? activity); + bool get onActivityPaused$async => false; + void onActivityPostCreated(jni$_.JObject? activity, jni$_.JObject? bundle); + bool get onActivityPostCreated$async => false; + void onActivityPostDestroyed(jni$_.JObject? activity); + bool get onActivityPostDestroyed$async => false; + void onActivityPostPaused(jni$_.JObject? activity); + bool get onActivityPostPaused$async => false; + void onActivityPostResumed(jni$_.JObject? activity); + bool get onActivityPostResumed$async => false; + void onActivityPostSaveInstanceState( + jni$_.JObject? activity, + jni$_.JObject? bundle, + ); + bool get onActivityPostSaveInstanceState$async => false; + void onActivityPostStarted(jni$_.JObject? activity); + bool get onActivityPostStarted$async => false; + void onActivityPostStopped(jni$_.JObject? activity); + bool get onActivityPostStopped$async => false; + void onActivityPreCreated(jni$_.JObject? activity, jni$_.JObject? bundle); + bool get onActivityPreCreated$async => false; + void onActivityPreDestroyed(jni$_.JObject? activity); + bool get onActivityPreDestroyed$async => false; + void onActivityPrePaused(jni$_.JObject? activity); + bool get onActivityPrePaused$async => false; + void onActivityPreResumed(jni$_.JObject? activity); + bool get onActivityPreResumed$async => false; + void onActivityPreSaveInstanceState( + jni$_.JObject? activity, + jni$_.JObject? bundle, + ); + bool get onActivityPreSaveInstanceState$async => false; + void onActivityPreStarted(jni$_.JObject? activity); + bool get onActivityPreStarted$async => false; + void onActivityPreStopped(jni$_.JObject? activity); + bool get onActivityPreStopped$async => false; + void onActivityResumed(jni$_.JObject? activity); + bool get onActivityResumed$async => false; + void onActivitySaveInstanceState( + jni$_.JObject? activity, + jni$_.JObject? bundle, + ); + bool get onActivitySaveInstanceState$async => false; + void onActivityStarted(jni$_.JObject? activity); + bool get onActivityStarted$async => false; + void onActivityStopped(jni$_.JObject? activity); + bool get onActivityStopped$async => false; +} + +final class _$Application$ActivityLifecycleCallbacks + with $Application$ActivityLifecycleCallbacks { + _$Application$ActivityLifecycleCallbacks({ + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityCreated, + this.onActivityCreated$async = false, + required void Function(jni$_.JObject? activity) onActivityDestroyed, + this.onActivityDestroyed$async = false, + required void Function(jni$_.JObject? activity) onActivityPaused, + this.onActivityPaused$async = false, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityPostCreated, + this.onActivityPostCreated$async = false, + required void Function(jni$_.JObject? activity) onActivityPostDestroyed, + this.onActivityPostDestroyed$async = false, + required void Function(jni$_.JObject? activity) onActivityPostPaused, + this.onActivityPostPaused$async = false, + required void Function(jni$_.JObject? activity) onActivityPostResumed, + this.onActivityPostResumed$async = false, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityPostSaveInstanceState, + this.onActivityPostSaveInstanceState$async = false, + required void Function(jni$_.JObject? activity) onActivityPostStarted, + this.onActivityPostStarted$async = false, + required void Function(jni$_.JObject? activity) onActivityPostStopped, + this.onActivityPostStopped$async = false, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityPreCreated, + this.onActivityPreCreated$async = false, + required void Function(jni$_.JObject? activity) onActivityPreDestroyed, + this.onActivityPreDestroyed$async = false, + required void Function(jni$_.JObject? activity) onActivityPrePaused, + this.onActivityPrePaused$async = false, + required void Function(jni$_.JObject? activity) onActivityPreResumed, + this.onActivityPreResumed$async = false, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivityPreSaveInstanceState, + this.onActivityPreSaveInstanceState$async = false, + required void Function(jni$_.JObject? activity) onActivityPreStarted, + this.onActivityPreStarted$async = false, + required void Function(jni$_.JObject? activity) onActivityPreStopped, + this.onActivityPreStopped$async = false, + required void Function(jni$_.JObject? activity) onActivityResumed, + this.onActivityResumed$async = false, + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onActivitySaveInstanceState, + this.onActivitySaveInstanceState$async = false, + required void Function(jni$_.JObject? activity) onActivityStarted, + this.onActivityStarted$async = false, + required void Function(jni$_.JObject? activity) onActivityStopped, + this.onActivityStopped$async = false, + }) : _onActivityCreated = onActivityCreated, + _onActivityDestroyed = onActivityDestroyed, + _onActivityPaused = onActivityPaused, + _onActivityPostCreated = onActivityPostCreated, + _onActivityPostDestroyed = onActivityPostDestroyed, + _onActivityPostPaused = onActivityPostPaused, + _onActivityPostResumed = onActivityPostResumed, + _onActivityPostSaveInstanceState = onActivityPostSaveInstanceState, + _onActivityPostStarted = onActivityPostStarted, + _onActivityPostStopped = onActivityPostStopped, + _onActivityPreCreated = onActivityPreCreated, + _onActivityPreDestroyed = onActivityPreDestroyed, + _onActivityPrePaused = onActivityPrePaused, + _onActivityPreResumed = onActivityPreResumed, + _onActivityPreSaveInstanceState = onActivityPreSaveInstanceState, + _onActivityPreStarted = onActivityPreStarted, + _onActivityPreStopped = onActivityPreStopped, + _onActivityResumed = onActivityResumed, + _onActivitySaveInstanceState = onActivitySaveInstanceState, + _onActivityStarted = onActivityStarted, + _onActivityStopped = onActivityStopped; + + final void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + _onActivityCreated; + final bool onActivityCreated$async; + final void Function(jni$_.JObject? activity) _onActivityDestroyed; + final bool onActivityDestroyed$async; + final void Function(jni$_.JObject? activity) _onActivityPaused; + final bool onActivityPaused$async; + final void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + _onActivityPostCreated; + final bool onActivityPostCreated$async; + final void Function(jni$_.JObject? activity) _onActivityPostDestroyed; + final bool onActivityPostDestroyed$async; + final void Function(jni$_.JObject? activity) _onActivityPostPaused; + final bool onActivityPostPaused$async; + final void Function(jni$_.JObject? activity) _onActivityPostResumed; + final bool onActivityPostResumed$async; + final void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + _onActivityPostSaveInstanceState; + final bool onActivityPostSaveInstanceState$async; + final void Function(jni$_.JObject? activity) _onActivityPostStarted; + final bool onActivityPostStarted$async; + final void Function(jni$_.JObject? activity) _onActivityPostStopped; + final bool onActivityPostStopped$async; + final void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + _onActivityPreCreated; + final bool onActivityPreCreated$async; + final void Function(jni$_.JObject? activity) _onActivityPreDestroyed; + final bool onActivityPreDestroyed$async; + final void Function(jni$_.JObject? activity) _onActivityPrePaused; + final bool onActivityPrePaused$async; + final void Function(jni$_.JObject? activity) _onActivityPreResumed; + final bool onActivityPreResumed$async; + final void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + _onActivityPreSaveInstanceState; + final bool onActivityPreSaveInstanceState$async; + final void Function(jni$_.JObject? activity) _onActivityPreStarted; + final bool onActivityPreStarted$async; + final void Function(jni$_.JObject? activity) _onActivityPreStopped; + final bool onActivityPreStopped$async; + final void Function(jni$_.JObject? activity) _onActivityResumed; + final bool onActivityResumed$async; + final void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + _onActivitySaveInstanceState; + final bool onActivitySaveInstanceState$async; + final void Function(jni$_.JObject? activity) _onActivityStarted; + final bool onActivityStarted$async; + final void Function(jni$_.JObject? activity) _onActivityStopped; + final bool onActivityStopped$async; + + void onActivityCreated(jni$_.JObject? activity, jni$_.JObject? bundle) { + return _onActivityCreated(activity, bundle); + } + + void onActivityDestroyed(jni$_.JObject? activity) { + return _onActivityDestroyed(activity); + } + + void onActivityPaused(jni$_.JObject? activity) { + return _onActivityPaused(activity); + } + + void onActivityPostCreated(jni$_.JObject? activity, jni$_.JObject? bundle) { + return _onActivityPostCreated(activity, bundle); + } + + void onActivityPostDestroyed(jni$_.JObject? activity) { + return _onActivityPostDestroyed(activity); + } + + void onActivityPostPaused(jni$_.JObject? activity) { + return _onActivityPostPaused(activity); + } + + void onActivityPostResumed(jni$_.JObject? activity) { + return _onActivityPostResumed(activity); + } + + void onActivityPostSaveInstanceState( + jni$_.JObject? activity, + jni$_.JObject? bundle, + ) { + return _onActivityPostSaveInstanceState(activity, bundle); + } + + void onActivityPostStarted(jni$_.JObject? activity) { + return _onActivityPostStarted(activity); + } + + void onActivityPostStopped(jni$_.JObject? activity) { + return _onActivityPostStopped(activity); + } + + void onActivityPreCreated(jni$_.JObject? activity, jni$_.JObject? bundle) { + return _onActivityPreCreated(activity, bundle); + } + + void onActivityPreDestroyed(jni$_.JObject? activity) { + return _onActivityPreDestroyed(activity); + } + + void onActivityPrePaused(jni$_.JObject? activity) { + return _onActivityPrePaused(activity); + } + + void onActivityPreResumed(jni$_.JObject? activity) { + return _onActivityPreResumed(activity); + } + + void onActivityPreSaveInstanceState( + jni$_.JObject? activity, + jni$_.JObject? bundle, + ) { + return _onActivityPreSaveInstanceState(activity, bundle); + } + + void onActivityPreStarted(jni$_.JObject? activity) { + return _onActivityPreStarted(activity); + } + + void onActivityPreStopped(jni$_.JObject? activity) { + return _onActivityPreStopped(activity); + } + + void onActivityResumed(jni$_.JObject? activity) { + return _onActivityResumed(activity); + } + + void onActivitySaveInstanceState( + jni$_.JObject? activity, + jni$_.JObject? bundle, + ) { + return _onActivitySaveInstanceState(activity, bundle); + } + + void onActivityStarted(jni$_.JObject? activity) { + return _onActivityStarted(activity); + } + + void onActivityStopped(jni$_.JObject? activity) { + return _onActivityStopped(activity); + } +} + +final class $Application$ActivityLifecycleCallbacks$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $Application$ActivityLifecycleCallbacks$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/app/Application$ActivityLifecycleCallbacks;'; + + @jni$_.internal + @core$_.override + Application$ActivityLifecycleCallbacks? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : Application$ActivityLifecycleCallbacks.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($Application$ActivityLifecycleCallbacks$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($Application$ActivityLifecycleCallbacks$NullableType$) && + other is $Application$ActivityLifecycleCallbacks$NullableType$; + } +} + +final class $Application$ActivityLifecycleCallbacks$Type$ + extends jni$_.JType { + @jni$_.internal + const $Application$ActivityLifecycleCallbacks$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/app/Application$ActivityLifecycleCallbacks;'; + + @jni$_.internal + @core$_.override + Application$ActivityLifecycleCallbacks fromReference( + jni$_.JReference reference, + ) => Application$ActivityLifecycleCallbacks.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $Application$ActivityLifecycleCallbacks$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Application$ActivityLifecycleCallbacks$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($Application$ActivityLifecycleCallbacks$Type$) && + other is $Application$ActivityLifecycleCallbacks$Type$; + } +} + +/// from: `android.app.Application$OnProvideAssistDataListener` +class Application$OnProvideAssistDataListener extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Application$OnProvideAssistDataListener.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/app/Application$OnProvideAssistDataListener', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType + nullableType = $Application$OnProvideAssistDataListener$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $Application$OnProvideAssistDataListener$Type$(); + static final _id_onProvideAssistData = _class.instanceMethodId( + r'onProvideAssistData', + r'(Landroid/app/Activity;Landroid/os/Bundle;)V', + ); + + static final _onProvideAssistData = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onProvideAssistData(android.app.Activity activity, android.os.Bundle bundle)` + void onProvideAssistData(jni$_.JObject? activity, jni$_.JObject? bundle) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _onProvideAssistData( + reference.pointer, + _id_onProvideAssistData as jni$_.JMethodIDPtr, + _$activity.pointer, + _$bundle.pointer, + ).check(); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map + _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses(0, descriptor.address, args.address), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == + r'onProvideAssistData(Landroid/app/Activity;Landroid/os/Bundle;)V') { + _$impls[$p]!.onProvideAssistData( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + $a![1]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $Application$OnProvideAssistDataListener $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'android.app.Application$OnProvideAssistDataListener', + $p, + _$invokePointer, + [ + if ($impl.onProvideAssistData$async) + r'onProvideAssistData(Landroid/app/Activity;Landroid/os/Bundle;)V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory Application$OnProvideAssistDataListener.implement( + $Application$OnProvideAssistDataListener $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return Application$OnProvideAssistDataListener.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $Application$OnProvideAssistDataListener { + factory $Application$OnProvideAssistDataListener({ + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onProvideAssistData, + bool onProvideAssistData$async, + }) = _$Application$OnProvideAssistDataListener; + + void onProvideAssistData(jni$_.JObject? activity, jni$_.JObject? bundle); + bool get onProvideAssistData$async => false; +} + +final class _$Application$OnProvideAssistDataListener + with $Application$OnProvideAssistDataListener { + _$Application$OnProvideAssistDataListener({ + required void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + onProvideAssistData, + this.onProvideAssistData$async = false, + }) : _onProvideAssistData = onProvideAssistData; + + final void Function(jni$_.JObject? activity, jni$_.JObject? bundle) + _onProvideAssistData; + final bool onProvideAssistData$async; + + void onProvideAssistData(jni$_.JObject? activity, jni$_.JObject? bundle) { + return _onProvideAssistData(activity, bundle); + } +} + +final class $Application$OnProvideAssistDataListener$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $Application$OnProvideAssistDataListener$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/app/Application$OnProvideAssistDataListener;'; + + @jni$_.internal + @core$_.override + Application$OnProvideAssistDataListener? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : Application$OnProvideAssistDataListener.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($Application$OnProvideAssistDataListener$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($Application$OnProvideAssistDataListener$NullableType$) && + other is $Application$OnProvideAssistDataListener$NullableType$; + } +} + +final class $Application$OnProvideAssistDataListener$Type$ + extends jni$_.JType { + @jni$_.internal + const $Application$OnProvideAssistDataListener$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/app/Application$OnProvideAssistDataListener;'; + + @jni$_.internal + @core$_.override + Application$OnProvideAssistDataListener fromReference( + jni$_.JReference reference, + ) => Application$OnProvideAssistDataListener.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $Application$OnProvideAssistDataListener$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Application$OnProvideAssistDataListener$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($Application$OnProvideAssistDataListener$Type$) && + other is $Application$OnProvideAssistDataListener$Type$; + } +} + +/// from: `android.app.Application` +class Application extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Application.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName(r'android/app/Application'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $Application$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Application$Type$(); + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory Application() { + return Application.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } + + static final _id_getProcessName = _class.staticMethodId( + r'getProcessName', + r'()Ljava/lang/String;', + ); + + static final _getProcessName = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `static public java.lang.String getProcessName()` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? getProcessName() { + return _getProcessName( + _class.reference.pointer, + _id_getProcessName as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_onConfigurationChanged = _class.instanceMethodId( + r'onConfigurationChanged', + r'(Landroid/content/res/Configuration;)V', + ); + + static final _onConfigurationChanged = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onConfigurationChanged(android.content.res.Configuration configuration)` + void onConfigurationChanged(jni$_.JObject? configuration) { + final _$configuration = configuration?.reference ?? jni$_.jNullReference; + _onConfigurationChanged( + reference.pointer, + _id_onConfigurationChanged as jni$_.JMethodIDPtr, + _$configuration.pointer, + ).check(); + } + + static final _id_onCreate = _class.instanceMethodId(r'onCreate', r'()V'); + + static final _onCreate = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void onCreate()` + void onCreate() { + _onCreate(reference.pointer, _id_onCreate as jni$_.JMethodIDPtr).check(); + } + + static final _id_onLowMemory = _class.instanceMethodId( + r'onLowMemory', + r'()V', + ); + + static final _onLowMemory = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void onLowMemory()` + void onLowMemory() { + _onLowMemory( + reference.pointer, + _id_onLowMemory as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_onTerminate = _class.instanceMethodId( + r'onTerminate', + r'()V', + ); + + static final _onTerminate = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void onTerminate()` + void onTerminate() { + _onTerminate( + reference.pointer, + _id_onTerminate as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_onTrimMemory = _class.instanceMethodId( + r'onTrimMemory', + r'(I)V', + ); + + static final _onTrimMemory = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public void onTrimMemory(int i)` + void onTrimMemory(int i) { + _onTrimMemory( + reference.pointer, + _id_onTrimMemory as jni$_.JMethodIDPtr, + i, + ).check(); + } + + static final _id_registerActivityLifecycleCallbacks = _class.instanceMethodId( + r'registerActivityLifecycleCallbacks', + r'(Landroid/app/Application$ActivityLifecycleCallbacks;)V', + ); + + static final _registerActivityLifecycleCallbacks = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void registerActivityLifecycleCallbacks(android.app.Application$ActivityLifecycleCallbacks activityLifecycleCallbacks)` + void registerActivityLifecycleCallbacks( + Application$ActivityLifecycleCallbacks? activityLifecycleCallbacks, + ) { + final _$activityLifecycleCallbacks = + activityLifecycleCallbacks?.reference ?? jni$_.jNullReference; + _registerActivityLifecycleCallbacks( + reference.pointer, + _id_registerActivityLifecycleCallbacks as jni$_.JMethodIDPtr, + _$activityLifecycleCallbacks.pointer, + ).check(); + } + + static final _id_registerComponentCallbacks = _class.instanceMethodId( + r'registerComponentCallbacks', + r'(Landroid/content/ComponentCallbacks;)V', + ); + + static final _registerComponentCallbacks = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void registerComponentCallbacks(android.content.ComponentCallbacks componentCallbacks)` + void registerComponentCallbacks(jni$_.JObject? componentCallbacks) { + final _$componentCallbacks = + componentCallbacks?.reference ?? jni$_.jNullReference; + _registerComponentCallbacks( + reference.pointer, + _id_registerComponentCallbacks as jni$_.JMethodIDPtr, + _$componentCallbacks.pointer, + ).check(); + } + + static final _id_registerOnProvideAssistDataListener = _class + .instanceMethodId( + r'registerOnProvideAssistDataListener', + r'(Landroid/app/Application$OnProvideAssistDataListener;)V', + ); + + static final _registerOnProvideAssistDataListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void registerOnProvideAssistDataListener(android.app.Application$OnProvideAssistDataListener onProvideAssistDataListener)` + void registerOnProvideAssistDataListener( + Application$OnProvideAssistDataListener? onProvideAssistDataListener, + ) { + final _$onProvideAssistDataListener = + onProvideAssistDataListener?.reference ?? jni$_.jNullReference; + _registerOnProvideAssistDataListener( + reference.pointer, + _id_registerOnProvideAssistDataListener as jni$_.JMethodIDPtr, + _$onProvideAssistDataListener.pointer, + ).check(); + } + + static final _id_unregisterActivityLifecycleCallbacks = _class + .instanceMethodId( + r'unregisterActivityLifecycleCallbacks', + r'(Landroid/app/Application$ActivityLifecycleCallbacks;)V', + ); + + static final _unregisterActivityLifecycleCallbacks = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void unregisterActivityLifecycleCallbacks(android.app.Application$ActivityLifecycleCallbacks activityLifecycleCallbacks)` + void unregisterActivityLifecycleCallbacks( + Application$ActivityLifecycleCallbacks? activityLifecycleCallbacks, + ) { + final _$activityLifecycleCallbacks = + activityLifecycleCallbacks?.reference ?? jni$_.jNullReference; + _unregisterActivityLifecycleCallbacks( + reference.pointer, + _id_unregisterActivityLifecycleCallbacks as jni$_.JMethodIDPtr, + _$activityLifecycleCallbacks.pointer, + ).check(); + } + + static final _id_unregisterComponentCallbacks = _class.instanceMethodId( + r'unregisterComponentCallbacks', + r'(Landroid/content/ComponentCallbacks;)V', + ); + + static final _unregisterComponentCallbacks = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void unregisterComponentCallbacks(android.content.ComponentCallbacks componentCallbacks)` + void unregisterComponentCallbacks(jni$_.JObject? componentCallbacks) { + final _$componentCallbacks = + componentCallbacks?.reference ?? jni$_.jNullReference; + _unregisterComponentCallbacks( + reference.pointer, + _id_unregisterComponentCallbacks as jni$_.JMethodIDPtr, + _$componentCallbacks.pointer, + ).check(); + } + + static final _id_unregisterOnProvideAssistDataListener = _class + .instanceMethodId( + r'unregisterOnProvideAssistDataListener', + r'(Landroid/app/Application$OnProvideAssistDataListener;)V', + ); + + static final _unregisterOnProvideAssistDataListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void unregisterOnProvideAssistDataListener(android.app.Application$OnProvideAssistDataListener onProvideAssistDataListener)` + void unregisterOnProvideAssistDataListener( + Application$OnProvideAssistDataListener? onProvideAssistDataListener, + ) { + final _$onProvideAssistDataListener = + onProvideAssistDataListener?.reference ?? jni$_.jNullReference; + _unregisterOnProvideAssistDataListener( + reference.pointer, + _id_unregisterOnProvideAssistDataListener as jni$_.JMethodIDPtr, + _$onProvideAssistDataListener.pointer, + ).check(); + } +} + +final class $Application$NullableType$ extends jni$_.JType { + @jni$_.internal + const $Application$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/app/Application;'; + + @jni$_.internal + @core$_.override + Application? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Application.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Application$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Application$NullableType$) && + other is $Application$NullableType$; + } +} + +final class $Application$Type$ extends jni$_.JType { + @jni$_.internal + const $Application$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/app/Application;'; + + @jni$_.internal + @core$_.override + Application fromReference(jni$_.JReference reference) => + Application.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $Application$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Application$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Application$Type$) && + other is $Application$Type$; + } +} + +/// from: `androidx.activity.ComponentActivity$NonConfigurationInstances` +class ComponentActivity$NonConfigurationInstances extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + ComponentActivity$NonConfigurationInstances.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/activity/ComponentActivity$NonConfigurationInstances', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType + nullableType = $ComponentActivity$NonConfigurationInstances$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $ComponentActivity$NonConfigurationInstances$Type$(); + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory ComponentActivity$NonConfigurationInstances() { + return ComponentActivity$NonConfigurationInstances.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } + + static final _id_getCustom = _class.instanceMethodId( + r'getCustom', + r'()Ljava/lang/Object;', + ); + + static final _getCustom = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public final java.lang.Object getCustom()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCustom() { + return _getCustom( + reference.pointer, + _id_getCustom as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_setCustom = _class.instanceMethodId( + r'setCustom', + r'(Ljava/lang/Object;)V', + ); + + static final _setCustom = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public final void setCustom(java.lang.Object object)` + void setCustom(jni$_.JObject? object) { + final _$object = object?.reference ?? jni$_.jNullReference; + _setCustom( + reference.pointer, + _id_setCustom as jni$_.JMethodIDPtr, + _$object.pointer, + ).check(); + } + + static final _id_getViewModelStore = _class.instanceMethodId( + r'getViewModelStore', + r'()Landroidx/lifecycle/ViewModelStore;', + ); + + static final _getViewModelStore = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public final androidx.lifecycle.ViewModelStore getViewModelStore()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getViewModelStore() { + return _getViewModelStore( + reference.pointer, + _id_getViewModelStore as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_setViewModelStore = _class.instanceMethodId( + r'setViewModelStore', + r'(Landroidx/lifecycle/ViewModelStore;)V', + ); + + static final _setViewModelStore = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public final void setViewModelStore(androidx.lifecycle.ViewModelStore viewModelStore)` + void setViewModelStore(jni$_.JObject? viewModelStore) { + final _$viewModelStore = viewModelStore?.reference ?? jni$_.jNullReference; + _setViewModelStore( + reference.pointer, + _id_setViewModelStore as jni$_.JMethodIDPtr, + _$viewModelStore.pointer, + ).check(); + } +} + +final class $ComponentActivity$NonConfigurationInstances$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $ComponentActivity$NonConfigurationInstances$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/activity/ComponentActivity$NonConfigurationInstances;'; + + @jni$_.internal + @core$_.override + ComponentActivity$NonConfigurationInstances? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : ComponentActivity$NonConfigurationInstances.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($ComponentActivity$NonConfigurationInstances$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ComponentActivity$NonConfigurationInstances$NullableType$) && + other is $ComponentActivity$NonConfigurationInstances$NullableType$; + } +} + +final class $ComponentActivity$NonConfigurationInstances$Type$ + extends jni$_.JType { + @jni$_.internal + const $ComponentActivity$NonConfigurationInstances$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/activity/ComponentActivity$NonConfigurationInstances;'; + + @jni$_.internal + @core$_.override + ComponentActivity$NonConfigurationInstances fromReference( + jni$_.JReference reference, + ) => ComponentActivity$NonConfigurationInstances.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $ComponentActivity$NonConfigurationInstances$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($ComponentActivity$NonConfigurationInstances$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ComponentActivity$NonConfigurationInstances$Type$) && + other is $ComponentActivity$NonConfigurationInstances$Type$; + } +} + +/// from: `androidx.activity.ComponentActivity` +class ComponentActivity extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + ComponentActivity.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/activity/ComponentActivity', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $ComponentActivity$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $ComponentActivity$Type$(); + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory ComponentActivity() { + return ComponentActivity.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } + + static final _id_getFullyDrawnReporter = _class.instanceMethodId( + r'getFullyDrawnReporter', + r'()Landroidx/activity/FullyDrawnReporter;', + ); + + static final _getFullyDrawnReporter = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public androidx.activity.FullyDrawnReporter getFullyDrawnReporter()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getFullyDrawnReporter() { + return _getFullyDrawnReporter( + reference.pointer, + _id_getFullyDrawnReporter as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_getActivityResultRegistry = _class.instanceMethodId( + r'getActivityResultRegistry', + r'()Landroidx/activity/result/ActivityResultRegistry;', + ); + + static final _getActivityResultRegistry = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public final androidx.activity.result.ActivityResultRegistry getActivityResultRegistry()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getActivityResultRegistry() { + return _getActivityResultRegistry( + reference.pointer, + _id_getActivityResultRegistry as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_new$1 = _class.constructorId(r'(I)V'); + + static final _new$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public void (int i)` + /// The returned object must be released after use, by calling the [release] method. + factory ComponentActivity.new$1(int i) { + return ComponentActivity.fromReference( + _new$1( + _class.reference.pointer, + _id_new$1 as jni$_.JMethodIDPtr, + i, + ).reference, + ); + } + + static final _id_onRetainNonConfigurationInstance = _class.instanceMethodId( + r'onRetainNonConfigurationInstance', + r'()Ljava/lang/Object;', + ); + + static final _onRetainNonConfigurationInstance = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun onRetainNonConfigurationInstance(): kotlin.Any?` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? onRetainNonConfigurationInstance() { + return _onRetainNonConfigurationInstance( + reference.pointer, + _id_onRetainNonConfigurationInstance as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_onRetainCustomNonConfigurationInstance = _class + .instanceMethodId( + r'onRetainCustomNonConfigurationInstance', + r'()Ljava/lang/Object;', + ); + + static final _onRetainCustomNonConfigurationInstance = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun onRetainCustomNonConfigurationInstance(): kotlin.Any?` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? onRetainCustomNonConfigurationInstance() { + return _onRetainCustomNonConfigurationInstance( + reference.pointer, + _id_onRetainCustomNonConfigurationInstance as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getLastCustomNonConfigurationInstance = _class + .instanceMethodId( + r'getLastCustomNonConfigurationInstance', + r'()Ljava/lang/Object;', + ); + + static final _getLastCustomNonConfigurationInstance = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.Object getLastCustomNonConfigurationInstance()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getLastCustomNonConfigurationInstance() { + return _getLastCustomNonConfigurationInstance( + reference.pointer, + _id_getLastCustomNonConfigurationInstance as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_setContentView = _class.instanceMethodId( + r'setContentView', + r'(I)V', + ); + + static final _setContentView = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public fun setContentView(layoutResID: kotlin.Int): kotlin.Unit` + void setContentView(int i) { + _setContentView( + reference.pointer, + _id_setContentView as jni$_.JMethodIDPtr, + i, + ).check(); + } + + static final _id_setContentView$1 = _class.instanceMethodId( + r'setContentView', + r'(Landroid/view/View;)V', + ); + + static final _setContentView$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun setContentView(view: android.view.View?): kotlin.Unit` + void setContentView$1(jni$_.JObject? view) { + final _$view = view?.reference ?? jni$_.jNullReference; + _setContentView$1( + reference.pointer, + _id_setContentView$1 as jni$_.JMethodIDPtr, + _$view.pointer, + ).check(); + } + + static final _id_setContentView$2 = _class.instanceMethodId( + r'setContentView', + r'(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V', + ); + + static final _setContentView$2 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun setContentView(view: android.view.View?, params: android.view.ViewGroup.LayoutParams?): kotlin.Unit` + void setContentView$2(jni$_.JObject? view, jni$_.JObject? layoutParams) { + final _$view = view?.reference ?? jni$_.jNullReference; + final _$layoutParams = layoutParams?.reference ?? jni$_.jNullReference; + _setContentView$2( + reference.pointer, + _id_setContentView$2 as jni$_.JMethodIDPtr, + _$view.pointer, + _$layoutParams.pointer, + ).check(); + } + + static final _id_addContentView = _class.instanceMethodId( + r'addContentView', + r'(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V', + ); + + static final _addContentView = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addContentView(view: android.view.View?, params: android.view.ViewGroup.LayoutParams?): kotlin.Unit` + void addContentView(jni$_.JObject? view, jni$_.JObject? layoutParams) { + final _$view = view?.reference ?? jni$_.jNullReference; + final _$layoutParams = layoutParams?.reference ?? jni$_.jNullReference; + _addContentView( + reference.pointer, + _id_addContentView as jni$_.JMethodIDPtr, + _$view.pointer, + _$layoutParams.pointer, + ).check(); + } + + static final _id_initializeViewTreeOwners = _class.instanceMethodId( + r'initializeViewTreeOwners', + r'()V', + ); + + static final _initializeViewTreeOwners = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun initializeViewTreeOwners(): kotlin.Unit` + void initializeViewTreeOwners() { + _initializeViewTreeOwners( + reference.pointer, + _id_initializeViewTreeOwners as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_peekAvailableContext = _class.instanceMethodId( + r'peekAvailableContext', + r'()Landroid/content/Context;', + ); + + static final _peekAvailableContext = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun peekAvailableContext(): android.content.Context?` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? peekAvailableContext() { + return _peekAvailableContext( + reference.pointer, + _id_peekAvailableContext as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_addOnContextAvailableListener = _class.instanceMethodId( + r'addOnContextAvailableListener', + r'(Landroidx/activity/contextaware/OnContextAvailableListener;)V', + ); + + static final _addOnContextAvailableListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addOnContextAvailableListener(listener: androidx.activity.contextaware.OnContextAvailableListener): kotlin.Unit` + void addOnContextAvailableListener(jni$_.JObject onContextAvailableListener) { + final _$onContextAvailableListener = onContextAvailableListener.reference; + _addOnContextAvailableListener( + reference.pointer, + _id_addOnContextAvailableListener as jni$_.JMethodIDPtr, + _$onContextAvailableListener.pointer, + ).check(); + } + + static final _id_removeOnContextAvailableListener = _class.instanceMethodId( + r'removeOnContextAvailableListener', + r'(Landroidx/activity/contextaware/OnContextAvailableListener;)V', + ); + + static final _removeOnContextAvailableListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun removeOnContextAvailableListener(listener: androidx.activity.contextaware.OnContextAvailableListener): kotlin.Unit` + void removeOnContextAvailableListener( + jni$_.JObject onContextAvailableListener, + ) { + final _$onContextAvailableListener = onContextAvailableListener.reference; + _removeOnContextAvailableListener( + reference.pointer, + _id_removeOnContextAvailableListener as jni$_.JMethodIDPtr, + _$onContextAvailableListener.pointer, + ).check(); + } + + static final _id_onPreparePanel = _class.instanceMethodId( + r'onPreparePanel', + r'(ILandroid/view/View;Landroid/view/Menu;)Z', + ); + + static final _onPreparePanel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Int32, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun onPreparePanel(featureId: kotlin.Int, view: android.view.View?, menu: android.view.Menu): kotlin.Boolean` + bool onPreparePanel(int i, jni$_.JObject? view, jni$_.JObject menu) { + final _$view = view?.reference ?? jni$_.jNullReference; + final _$menu = menu.reference; + return _onPreparePanel( + reference.pointer, + _id_onPreparePanel as jni$_.JMethodIDPtr, + i, + _$view.pointer, + _$menu.pointer, + ).boolean; + } + + static final _id_onCreatePanelMenu = _class.instanceMethodId( + r'onCreatePanelMenu', + r'(ILandroid/view/Menu;)Z', + ); + + static final _onCreatePanelMenu = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public fun onCreatePanelMenu(featureId: kotlin.Int, menu: android.view.Menu): kotlin.Boolean` + bool onCreatePanelMenu(int i, jni$_.JObject menu) { + final _$menu = menu.reference; + return _onCreatePanelMenu( + reference.pointer, + _id_onCreatePanelMenu as jni$_.JMethodIDPtr, + i, + _$menu.pointer, + ).boolean; + } + + static final _id_onMenuItemSelected = _class.instanceMethodId( + r'onMenuItemSelected', + r'(ILandroid/view/MenuItem;)Z', + ); + + static final _onMenuItemSelected = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public fun onMenuItemSelected(featureId: kotlin.Int, item: android.view.MenuItem): kotlin.Boolean` + bool onMenuItemSelected(int i, jni$_.JObject menuItem) { + final _$menuItem = menuItem.reference; + return _onMenuItemSelected( + reference.pointer, + _id_onMenuItemSelected as jni$_.JMethodIDPtr, + i, + _$menuItem.pointer, + ).boolean; + } + + static final _id_onPanelClosed = _class.instanceMethodId( + r'onPanelClosed', + r'(ILandroid/view/Menu;)V', + ); + + static final _onPanelClosed = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public fun onPanelClosed(featureId: kotlin.Int, menu: android.view.Menu): kotlin.Unit` + void onPanelClosed(int i, jni$_.JObject menu) { + final _$menu = menu.reference; + _onPanelClosed( + reference.pointer, + _id_onPanelClosed as jni$_.JMethodIDPtr, + i, + _$menu.pointer, + ).check(); + } + + static final _id_addMenuProvider = _class.instanceMethodId( + r'addMenuProvider', + r'(Landroidx/core/view/MenuProvider;)V', + ); + + static final _addMenuProvider = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addMenuProvider(provider: androidx.core.view.MenuProvider): kotlin.Unit` + void addMenuProvider(jni$_.JObject menuProvider) { + final _$menuProvider = menuProvider.reference; + _addMenuProvider( + reference.pointer, + _id_addMenuProvider as jni$_.JMethodIDPtr, + _$menuProvider.pointer, + ).check(); + } + + static final _id_addMenuProvider$1 = _class.instanceMethodId( + r'addMenuProvider', + r'(Landroidx/core/view/MenuProvider;Landroidx/lifecycle/LifecycleOwner;)V', + ); + + static final _addMenuProvider$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addMenuProvider(provider: androidx.core.view.MenuProvider, owner: androidx.lifecycle.LifecycleOwner): kotlin.Unit` + void addMenuProvider$1( + jni$_.JObject menuProvider, + jni$_.JObject lifecycleOwner, + ) { + final _$menuProvider = menuProvider.reference; + final _$lifecycleOwner = lifecycleOwner.reference; + _addMenuProvider$1( + reference.pointer, + _id_addMenuProvider$1 as jni$_.JMethodIDPtr, + _$menuProvider.pointer, + _$lifecycleOwner.pointer, + ).check(); + } + + static final _id_addMenuProvider$2 = _class.instanceMethodId( + r'addMenuProvider', + r'(Landroidx/core/view/MenuProvider;Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$State;)V', + ); + + static final _addMenuProvider$2 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addMenuProvider(provider: androidx.core.view.MenuProvider, owner: androidx.lifecycle.LifecycleOwner, state: androidx.lifecycle.Lifecycle.State): kotlin.Unit` + void addMenuProvider$2( + jni$_.JObject menuProvider, + jni$_.JObject lifecycleOwner, + jni$_.JObject state, + ) { + final _$menuProvider = menuProvider.reference; + final _$lifecycleOwner = lifecycleOwner.reference; + final _$state = state.reference; + _addMenuProvider$2( + reference.pointer, + _id_addMenuProvider$2 as jni$_.JMethodIDPtr, + _$menuProvider.pointer, + _$lifecycleOwner.pointer, + _$state.pointer, + ).check(); + } + + static final _id_removeMenuProvider = _class.instanceMethodId( + r'removeMenuProvider', + r'(Landroidx/core/view/MenuProvider;)V', + ); + + static final _removeMenuProvider = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun removeMenuProvider(provider: androidx.core.view.MenuProvider): kotlin.Unit` + void removeMenuProvider(jni$_.JObject menuProvider) { + final _$menuProvider = menuProvider.reference; + _removeMenuProvider( + reference.pointer, + _id_removeMenuProvider as jni$_.JMethodIDPtr, + _$menuProvider.pointer, + ).check(); + } + + static final _id_invalidateMenu = _class.instanceMethodId( + r'invalidateMenu', + r'()V', + ); + + static final _invalidateMenu = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun invalidateMenu(): kotlin.Unit` + void invalidateMenu() { + _invalidateMenu( + reference.pointer, + _id_invalidateMenu as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_getLifecycle = _class.instanceMethodId( + r'getLifecycle', + r'()Landroidx/lifecycle/Lifecycle;', + ); + + static final _getLifecycle = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public androidx.lifecycle.Lifecycle getLifecycle()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getLifecycle() { + return _getLifecycle( + reference.pointer, + _id_getLifecycle as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_getViewModelStore = _class.instanceMethodId( + r'getViewModelStore', + r'()Landroidx/lifecycle/ViewModelStore;', + ); + + static final _getViewModelStore = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public androidx.lifecycle.ViewModelStore getViewModelStore()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getViewModelStore() { + return _getViewModelStore( + reference.pointer, + _id_getViewModelStore as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_getDefaultViewModelProviderFactory = _class.instanceMethodId( + r'getDefaultViewModelProviderFactory', + r'()Landroidx/lifecycle/ViewModelProvider$Factory;', + ); + + static final _getDefaultViewModelProviderFactory = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public androidx.lifecycle.ViewModelProvider$Factory getDefaultViewModelProviderFactory()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getDefaultViewModelProviderFactory() { + return _getDefaultViewModelProviderFactory( + reference.pointer, + _id_getDefaultViewModelProviderFactory as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_getDefaultViewModelCreationExtras = _class.instanceMethodId( + r'getDefaultViewModelCreationExtras', + r'()Landroidx/lifecycle/viewmodel/CreationExtras;', + ); + + static final _getDefaultViewModelCreationExtras = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getDefaultViewModelCreationExtras() { + return _getDefaultViewModelCreationExtras( + reference.pointer, + _id_getDefaultViewModelCreationExtras as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_onBackPressed = _class.instanceMethodId( + r'onBackPressed', + r'()V', + ); + + static final _onBackPressed = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun onBackPressed(): kotlin.Unit` + void onBackPressed() { + _onBackPressed( + reference.pointer, + _id_onBackPressed as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_getOnBackPressedDispatcher = _class.instanceMethodId( + r'getOnBackPressedDispatcher', + r'()Landroidx/activity/OnBackPressedDispatcher;', + ); + + static final _getOnBackPressedDispatcher = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getOnBackPressedDispatcher() { + return _getOnBackPressedDispatcher( + reference.pointer, + _id_getOnBackPressedDispatcher as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_getNavigationEventDispatcher = _class.instanceMethodId( + r'getNavigationEventDispatcher', + r'()Landroidx/navigationevent/NavigationEventDispatcher;', + ); + + static final _getNavigationEventDispatcher = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public androidx.navigationevent.NavigationEventDispatcher getNavigationEventDispatcher()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getNavigationEventDispatcher() { + return _getNavigationEventDispatcher( + reference.pointer, + _id_getNavigationEventDispatcher as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_getSavedStateRegistry = _class.instanceMethodId( + r'getSavedStateRegistry', + r'()Landroidx/savedstate/SavedStateRegistry;', + ); + + static final _getSavedStateRegistry = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public final androidx.savedstate.SavedStateRegistry getSavedStateRegistry()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getSavedStateRegistry() { + return _getSavedStateRegistry( + reference.pointer, + _id_getSavedStateRegistry as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_startActivityForResult = _class.instanceMethodId( + r'startActivityForResult', + r'(Landroid/content/Intent;I)V', + ); + + static final _startActivityForResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public fun startActivityForResult(intent: android.content.Intent, requestCode: kotlin.Int): kotlin.Unit` + void startActivityForResult(Intent intent, int i) { + final _$intent = intent.reference; + _startActivityForResult( + reference.pointer, + _id_startActivityForResult as jni$_.JMethodIDPtr, + _$intent.pointer, + i, + ).check(); + } + + static final _id_startActivityForResult$1 = _class.instanceMethodId( + r'startActivityForResult', + r'(Landroid/content/Intent;ILandroid/os/Bundle;)V', + ); + + static final _startActivityForResult$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public fun startActivityForResult(intent: android.content.Intent, requestCode: kotlin.Int, options: android.os.Bundle?): kotlin.Unit` + void startActivityForResult$1(Intent intent, int i, jni$_.JObject? bundle) { + final _$intent = intent.reference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startActivityForResult$1( + reference.pointer, + _id_startActivityForResult$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + i, + _$bundle.pointer, + ).check(); + } + + static final _id_startIntentSenderForResult = _class.instanceMethodId( + r'startIntentSenderForResult', + r'(Landroid/content/IntentSender;ILandroid/content/Intent;III)V', + ); + + static final _startIntentSenderForResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + jni$_.Int32, + jni$_.Int32, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + jni$_.Pointer, + int, + int, + int, + ) + >(); + + /// from: `public fun startIntentSenderForResult(intent: android.content.IntentSender, requestCode: kotlin.Int, fillInIntent: android.content.Intent?, flagsMask: kotlin.Int, flagsValues: kotlin.Int, extraFlags: kotlin.Int): kotlin.Unit` + void startIntentSenderForResult( + jni$_.JObject intentSender, + int i, + Intent? intent, + int i1, + int i2, + int i3, + ) { + final _$intentSender = intentSender.reference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + _startIntentSenderForResult( + reference.pointer, + _id_startIntentSenderForResult as jni$_.JMethodIDPtr, + _$intentSender.pointer, + i, + _$intent.pointer, + i1, + i2, + i3, + ).check(); + } + + static final _id_startIntentSenderForResult$1 = _class.instanceMethodId( + r'startIntentSenderForResult', + r'(Landroid/content/IntentSender;ILandroid/content/Intent;IIILandroid/os/Bundle;)V', + ); + + static final _startIntentSenderForResult$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + jni$_.Int32, + jni$_.Int32, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + jni$_.Pointer, + int, + int, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public fun startIntentSenderForResult(intent: android.content.IntentSender, requestCode: kotlin.Int, fillInIntent: android.content.Intent?, flagsMask: kotlin.Int, flagsValues: kotlin.Int, extraFlags: kotlin.Int, options: android.os.Bundle?): kotlin.Unit` + void startIntentSenderForResult$1( + jni$_.JObject intentSender, + int i, + Intent? intent, + int i1, + int i2, + int i3, + jni$_.JObject? bundle, + ) { + final _$intentSender = intentSender.reference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startIntentSenderForResult$1( + reference.pointer, + _id_startIntentSenderForResult$1 as jni$_.JMethodIDPtr, + _$intentSender.pointer, + i, + _$intent.pointer, + i1, + i2, + i3, + _$bundle.pointer, + ).check(); + } + + static final _id_onRequestPermissionsResult = _class.instanceMethodId( + r'onRequestPermissionsResult', + r'(I[Ljava/lang/String;[I)V', + ); + + static final _onRequestPermissionsResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Int32, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun onRequestPermissionsResult(requestCode: kotlin.Int, permissions: kotlin.Array, grantResults: kotlin.IntArray): kotlin.Unit` + void onRequestPermissionsResult( + int i, + jni$_.JArray strings, + jni$_.JIntArray is$, + ) { + final _$strings = strings.reference; + final _$is$ = is$.reference; + _onRequestPermissionsResult( + reference.pointer, + _id_onRequestPermissionsResult as jni$_.JMethodIDPtr, + i, + _$strings.pointer, + _$is$.pointer, + ).check(); + } + + static final _id_registerForActivityResult = _class.instanceMethodId( + r'registerForActivityResult', + r'(Landroidx/activity/result/contract/ActivityResultContract;Landroidx/activity/result/ActivityResultRegistry;Landroidx/activity/result/ActivityResultCallback;)Landroidx/activity/result/ActivityResultLauncher;', + ); + + static final _registerForActivityResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun registerForActivityResult(contract: androidx.activity.result.contract.ActivityResultContract, registry: androidx.activity.result.ActivityResultRegistry, callback: androidx.activity.result.ActivityResultCallback): androidx.activity.result.ActivityResultLauncher` + /// The returned object must be released after use, by calling the [release] method. + ActivityResultLauncher<$I> registerForActivityResult< + $I extends jni$_.JObject?, + $O extends jni$_.JObject? + >( + ActivityResultContract<$I, $O> activityResultContract, + jni$_.JObject activityResultRegistry, + ActivityResultCallback<$O> activityResultCallback, { + jni$_.JType<$I>? I, + jni$_.JType<$O>? O, + }) { + I ??= + jni$_.lowestCommonSuperType([ + (activityResultContract.$type + as $ActivityResultContract$Type$< + core$_.dynamic, + core$_.dynamic + >) + .I, + ]) + as jni$_.JType<$I>; + O ??= + jni$_.lowestCommonSuperType([ + (activityResultCallback.$type + as $ActivityResultCallback$Type$) + .O, + (activityResultContract.$type + as $ActivityResultContract$Type$< + core$_.dynamic, + core$_.dynamic + >) + .O, + ]) + as jni$_.JType<$O>; + final _$activityResultContract = activityResultContract.reference; + final _$activityResultRegistry = activityResultRegistry.reference; + final _$activityResultCallback = activityResultCallback.reference; + return _registerForActivityResult( + reference.pointer, + _id_registerForActivityResult as jni$_.JMethodIDPtr, + _$activityResultContract.pointer, + _$activityResultRegistry.pointer, + _$activityResultCallback.pointer, + ).object>($ActivityResultLauncher$Type$<$I>(I)); + } + + static final _id_registerForActivityResult$1 = _class.instanceMethodId( + r'registerForActivityResult', + r'(Landroidx/activity/result/contract/ActivityResultContract;Landroidx/activity/result/ActivityResultCallback;)Landroidx/activity/result/ActivityResultLauncher;', + ); + + static final _registerForActivityResult$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun registerForActivityResult(contract: androidx.activity.result.contract.ActivityResultContract, callback: androidx.activity.result.ActivityResultCallback): androidx.activity.result.ActivityResultLauncher` + /// The returned object must be released after use, by calling the [release] method. + ActivityResultLauncher<$I> registerForActivityResult$1< + $I extends jni$_.JObject?, + $O extends jni$_.JObject? + >( + ActivityResultContract<$I, $O> activityResultContract, + ActivityResultCallback<$O> activityResultCallback, { + jni$_.JType<$I>? I, + jni$_.JType<$O>? O, + }) { + I ??= + jni$_.lowestCommonSuperType([ + (activityResultContract.$type + as $ActivityResultContract$Type$< + core$_.dynamic, + core$_.dynamic + >) + .I, + ]) + as jni$_.JType<$I>; + O ??= + jni$_.lowestCommonSuperType([ + (activityResultCallback.$type + as $ActivityResultCallback$Type$) + .O, + (activityResultContract.$type + as $ActivityResultContract$Type$< + core$_.dynamic, + core$_.dynamic + >) + .O, + ]) + as jni$_.JType<$O>; + final _$activityResultContract = activityResultContract.reference; + final _$activityResultCallback = activityResultCallback.reference; + return _registerForActivityResult$1( + reference.pointer, + _id_registerForActivityResult$1 as jni$_.JMethodIDPtr, + _$activityResultContract.pointer, + _$activityResultCallback.pointer, + ).object>($ActivityResultLauncher$Type$<$I>(I)); + } + + static final _id_onConfigurationChanged = _class.instanceMethodId( + r'onConfigurationChanged', + r'(Landroid/content/res/Configuration;)V', + ); + + static final _onConfigurationChanged = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun onConfigurationChanged(newConfig: android.content.res.Configuration): kotlin.Unit` + void onConfigurationChanged(jni$_.JObject configuration) { + final _$configuration = configuration.reference; + _onConfigurationChanged( + reference.pointer, + _id_onConfigurationChanged as jni$_.JMethodIDPtr, + _$configuration.pointer, + ).check(); + } + + static final _id_addOnConfigurationChangedListener = _class.instanceMethodId( + r'addOnConfigurationChangedListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _addOnConfigurationChangedListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addOnConfigurationChangedListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void addOnConfigurationChangedListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _addOnConfigurationChangedListener( + reference.pointer, + _id_addOnConfigurationChangedListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_removeOnConfigurationChangedListener = _class + .instanceMethodId( + r'removeOnConfigurationChangedListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _removeOnConfigurationChangedListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun removeOnConfigurationChangedListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void removeOnConfigurationChangedListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _removeOnConfigurationChangedListener( + reference.pointer, + _id_removeOnConfigurationChangedListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_onTrimMemory = _class.instanceMethodId( + r'onTrimMemory', + r'(I)V', + ); + + static final _onTrimMemory = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public fun onTrimMemory(level: kotlin.Int): kotlin.Unit` + void onTrimMemory(int i) { + _onTrimMemory( + reference.pointer, + _id_onTrimMemory as jni$_.JMethodIDPtr, + i, + ).check(); + } + + static final _id_addOnTrimMemoryListener = _class.instanceMethodId( + r'addOnTrimMemoryListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _addOnTrimMemoryListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addOnTrimMemoryListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void addOnTrimMemoryListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _addOnTrimMemoryListener( + reference.pointer, + _id_addOnTrimMemoryListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_removeOnTrimMemoryListener = _class.instanceMethodId( + r'removeOnTrimMemoryListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _removeOnTrimMemoryListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun removeOnTrimMemoryListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void removeOnTrimMemoryListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _removeOnTrimMemoryListener( + reference.pointer, + _id_removeOnTrimMemoryListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_addOnNewIntentListener = _class.instanceMethodId( + r'addOnNewIntentListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _addOnNewIntentListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addOnNewIntentListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void addOnNewIntentListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _addOnNewIntentListener( + reference.pointer, + _id_addOnNewIntentListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_removeOnNewIntentListener = _class.instanceMethodId( + r'removeOnNewIntentListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _removeOnNewIntentListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun removeOnNewIntentListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void removeOnNewIntentListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _removeOnNewIntentListener( + reference.pointer, + _id_removeOnNewIntentListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_onMultiWindowModeChanged = _class.instanceMethodId( + r'onMultiWindowModeChanged', + r'(Z)V', + ); + + static final _onMultiWindowModeChanged = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public fun onMultiWindowModeChanged(isInMultiWindowMode: kotlin.Boolean): kotlin.Unit` + void onMultiWindowModeChanged(bool z) { + _onMultiWindowModeChanged( + reference.pointer, + _id_onMultiWindowModeChanged as jni$_.JMethodIDPtr, + z ? 1 : 0, + ).check(); + } + + static final _id_onMultiWindowModeChanged$1 = _class.instanceMethodId( + r'onMultiWindowModeChanged', + r'(ZLandroid/content/res/Configuration;)V', + ); + + static final _onMultiWindowModeChanged$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public fun onMultiWindowModeChanged(isInMultiWindowMode: kotlin.Boolean, newConfig: android.content.res.Configuration): kotlin.Unit` + void onMultiWindowModeChanged$1(bool z, jni$_.JObject configuration) { + final _$configuration = configuration.reference; + _onMultiWindowModeChanged$1( + reference.pointer, + _id_onMultiWindowModeChanged$1 as jni$_.JMethodIDPtr, + z ? 1 : 0, + _$configuration.pointer, + ).check(); + } + + static final _id_addOnMultiWindowModeChangedListener = _class + .instanceMethodId( + r'addOnMultiWindowModeChangedListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _addOnMultiWindowModeChangedListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addOnMultiWindowModeChangedListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void addOnMultiWindowModeChangedListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _addOnMultiWindowModeChangedListener( + reference.pointer, + _id_addOnMultiWindowModeChangedListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_removeOnMultiWindowModeChangedListener = _class + .instanceMethodId( + r'removeOnMultiWindowModeChangedListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _removeOnMultiWindowModeChangedListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun removeOnMultiWindowModeChangedListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void removeOnMultiWindowModeChangedListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _removeOnMultiWindowModeChangedListener( + reference.pointer, + _id_removeOnMultiWindowModeChangedListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_onPictureInPictureModeChanged = _class.instanceMethodId( + r'onPictureInPictureModeChanged', + r'(Z)V', + ); + + static final _onPictureInPictureModeChanged = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public fun onPictureInPictureModeChanged(isInPictureInPictureMode: kotlin.Boolean): kotlin.Unit` + void onPictureInPictureModeChanged(bool z) { + _onPictureInPictureModeChanged( + reference.pointer, + _id_onPictureInPictureModeChanged as jni$_.JMethodIDPtr, + z ? 1 : 0, + ).check(); + } + + static final _id_onPictureInPictureModeChanged$1 = _class.instanceMethodId( + r'onPictureInPictureModeChanged', + r'(ZLandroid/content/res/Configuration;)V', + ); + + static final _onPictureInPictureModeChanged$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public fun onPictureInPictureModeChanged(isInPictureInPictureMode: kotlin.Boolean, newConfig: android.content.res.Configuration): kotlin.Unit` + void onPictureInPictureModeChanged$1(bool z, jni$_.JObject configuration) { + final _$configuration = configuration.reference; + _onPictureInPictureModeChanged$1( + reference.pointer, + _id_onPictureInPictureModeChanged$1 as jni$_.JMethodIDPtr, + z ? 1 : 0, + _$configuration.pointer, + ).check(); + } + + static final _id_addOnPictureInPictureModeChangedListener = _class + .instanceMethodId( + r'addOnPictureInPictureModeChangedListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _addOnPictureInPictureModeChangedListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addOnPictureInPictureModeChangedListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void addOnPictureInPictureModeChangedListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _addOnPictureInPictureModeChangedListener( + reference.pointer, + _id_addOnPictureInPictureModeChangedListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_removeOnPictureInPictureModeChangedListener = _class + .instanceMethodId( + r'removeOnPictureInPictureModeChangedListener', + r'(Landroidx/core/util/Consumer;)V', + ); + + static final _removeOnPictureInPictureModeChangedListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun removeOnPictureInPictureModeChangedListener(listener: androidx.core.util.Consumer): kotlin.Unit` + void removeOnPictureInPictureModeChangedListener(jni$_.JObject consumer) { + final _$consumer = consumer.reference; + _removeOnPictureInPictureModeChangedListener( + reference.pointer, + _id_removeOnPictureInPictureModeChangedListener as jni$_.JMethodIDPtr, + _$consumer.pointer, + ).check(); + } + + static final _id_addOnUserLeaveHintListener = _class.instanceMethodId( + r'addOnUserLeaveHintListener', + r'(Ljava/lang/Runnable;)V', + ); + + static final _addOnUserLeaveHintListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun addOnUserLeaveHintListener(listener: java.lang.Runnable): kotlin.Unit` + void addOnUserLeaveHintListener(jni$_.JObject runnable) { + final _$runnable = runnable.reference; + _addOnUserLeaveHintListener( + reference.pointer, + _id_addOnUserLeaveHintListener as jni$_.JMethodIDPtr, + _$runnable.pointer, + ).check(); + } + + static final _id_removeOnUserLeaveHintListener = _class.instanceMethodId( + r'removeOnUserLeaveHintListener', + r'(Ljava/lang/Runnable;)V', + ); + + static final _removeOnUserLeaveHintListener = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun removeOnUserLeaveHintListener(listener: java.lang.Runnable): kotlin.Unit` + void removeOnUserLeaveHintListener(jni$_.JObject runnable) { + final _$runnable = runnable.reference; + _removeOnUserLeaveHintListener( + reference.pointer, + _id_removeOnUserLeaveHintListener as jni$_.JMethodIDPtr, + _$runnable.pointer, + ).check(); + } + + static final _id_reportFullyDrawn = _class.instanceMethodId( + r'reportFullyDrawn', + r'()V', + ); + + static final _reportFullyDrawn = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun reportFullyDrawn(): kotlin.Unit` + void reportFullyDrawn() { + _reportFullyDrawn( + reference.pointer, + _id_reportFullyDrawn as jni$_.JMethodIDPtr, + ).check(); + } +} + +final class $ComponentActivity$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $ComponentActivity$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/activity/ComponentActivity;'; + + @jni$_.internal + @core$_.override + ComponentActivity? fromReference(jni$_.JReference reference) => + reference.isNull ? null : ComponentActivity.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ComponentActivity$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ComponentActivity$NullableType$) && + other is $ComponentActivity$NullableType$; + } +} + +final class $ComponentActivity$Type$ extends jni$_.JType { + @jni$_.internal + const $ComponentActivity$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/activity/ComponentActivity;'; + + @jni$_.internal + @core$_.override + ComponentActivity fromReference(jni$_.JReference reference) => + ComponentActivity.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $ComponentActivity$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ComponentActivity$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ComponentActivity$Type$) && + other is $ComponentActivity$Type$; + } +} + +/// from: `androidx.fragment.app.FragmentActivity` +class FragmentActivity extends ComponentActivity { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + FragmentActivity.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/fragment/app/FragmentActivity', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $FragmentActivity$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $FragmentActivity$Type$(); + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory FragmentActivity() { + return FragmentActivity.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } + + static final _id_new1 = _class.constructorId(r'(I)V'); + + static final _new1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public void (int i)` + /// The returned object must be released after use, by calling the [release] method. + factory FragmentActivity.new1(int i) { + return FragmentActivity.fromReference( + _new1( + _class.reference.pointer, + _id_new1 as jni$_.JMethodIDPtr, + i, + ).reference, + ); + } + + static final _id_supportFinishAfterTransition = _class.instanceMethodId( + r'supportFinishAfterTransition', + r'()V', + ); + + static final _supportFinishAfterTransition = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void supportFinishAfterTransition()` + void supportFinishAfterTransition() { + _supportFinishAfterTransition( + reference.pointer, + _id_supportFinishAfterTransition as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_setEnterSharedElementCallback = _class.instanceMethodId( + r'setEnterSharedElementCallback', + r'(Landroidx/core/app/SharedElementCallback;)V', + ); + + static final _setEnterSharedElementCallback = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void setEnterSharedElementCallback(androidx.core.app.SharedElementCallback sharedElementCallback)` + void setEnterSharedElementCallback(jni$_.JObject? sharedElementCallback) { + final _$sharedElementCallback = + sharedElementCallback?.reference ?? jni$_.jNullReference; + _setEnterSharedElementCallback( + reference.pointer, + _id_setEnterSharedElementCallback as jni$_.JMethodIDPtr, + _$sharedElementCallback.pointer, + ).check(); + } + + static final _id_setExitSharedElementCallback = _class.instanceMethodId( + r'setExitSharedElementCallback', + r'(Landroidx/core/app/SharedElementCallback;)V', + ); + + static final _setExitSharedElementCallback = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void setExitSharedElementCallback(androidx.core.app.SharedElementCallback sharedElementCallback)` + void setExitSharedElementCallback(jni$_.JObject? sharedElementCallback) { + final _$sharedElementCallback = + sharedElementCallback?.reference ?? jni$_.jNullReference; + _setExitSharedElementCallback( + reference.pointer, + _id_setExitSharedElementCallback as jni$_.JMethodIDPtr, + _$sharedElementCallback.pointer, + ).check(); + } + + static final _id_supportPostponeEnterTransition = _class.instanceMethodId( + r'supportPostponeEnterTransition', + r'()V', + ); + + static final _supportPostponeEnterTransition = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void supportPostponeEnterTransition()` + void supportPostponeEnterTransition() { + _supportPostponeEnterTransition( + reference.pointer, + _id_supportPostponeEnterTransition as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_supportStartPostponedEnterTransition = _class + .instanceMethodId(r'supportStartPostponedEnterTransition', r'()V'); + + static final _supportStartPostponedEnterTransition = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void supportStartPostponedEnterTransition()` + void supportStartPostponedEnterTransition() { + _supportStartPostponedEnterTransition( + reference.pointer, + _id_supportStartPostponedEnterTransition as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_onCreateView = _class.instanceMethodId( + r'onCreateView', + r'(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;', + ); + + static final _onCreateView = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.view.View onCreateView(android.view.View view, java.lang.String string, android.content.Context context, android.util.AttributeSet attributeSet)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? onCreateView( + jni$_.JObject? view, + jni$_.JString string, + jni$_.JObject context, + jni$_.JObject attributeSet, + ) { + final _$view = view?.reference ?? jni$_.jNullReference; + final _$string = string.reference; + final _$context = context.reference; + final _$attributeSet = attributeSet.reference; + return _onCreateView( + reference.pointer, + _id_onCreateView as jni$_.JMethodIDPtr, + _$view.pointer, + _$string.pointer, + _$context.pointer, + _$attributeSet.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_onCreateView$1 = _class.instanceMethodId( + r'onCreateView', + r'(Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;', + ); + + static final _onCreateView$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.view.View onCreateView(java.lang.String string, android.content.Context context, android.util.AttributeSet attributeSet)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? onCreateView$1( + jni$_.JString string, + jni$_.JObject context, + jni$_.JObject attributeSet, + ) { + final _$string = string.reference; + final _$context = context.reference; + final _$attributeSet = attributeSet.reference; + return _onCreateView$1( + reference.pointer, + _id_onCreateView$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$context.pointer, + _$attributeSet.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_onMenuItemSelected = _class.instanceMethodId( + r'onMenuItemSelected', + r'(ILandroid/view/MenuItem;)Z', + ); + + static final _onMenuItemSelected = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean onMenuItemSelected(int i, android.view.MenuItem menuItem)` + bool onMenuItemSelected(int i, jni$_.JObject menuItem) { + final _$menuItem = menuItem.reference; + return _onMenuItemSelected( + reference.pointer, + _id_onMenuItemSelected as jni$_.JMethodIDPtr, + i, + _$menuItem.pointer, + ).boolean; + } + + static final _id_onStateNotSaved = _class.instanceMethodId( + r'onStateNotSaved', + r'()V', + ); + + static final _onStateNotSaved = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void onStateNotSaved()` + void onStateNotSaved() { + _onStateNotSaved( + reference.pointer, + _id_onStateNotSaved as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_supportInvalidateOptionsMenu = _class.instanceMethodId( + r'supportInvalidateOptionsMenu', + r'()V', + ); + + static final _supportInvalidateOptionsMenu = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void supportInvalidateOptionsMenu()` + void supportInvalidateOptionsMenu() { + _supportInvalidateOptionsMenu( + reference.pointer, + _id_supportInvalidateOptionsMenu as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_dump = _class.instanceMethodId( + r'dump', + r'(Ljava/lang/String;Ljava/io/FileDescriptor;Ljava/io/PrintWriter;[Ljava/lang/String;)V', + ); + + static final _dump = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void dump(java.lang.String string, java.io.FileDescriptor fileDescriptor, java.io.PrintWriter printWriter, java.lang.String[] strings)` + void dump( + jni$_.JString string, + jni$_.JObject? fileDescriptor, + jni$_.JObject printWriter, + jni$_.JArray? strings, + ) { + final _$string = string.reference; + final _$fileDescriptor = fileDescriptor?.reference ?? jni$_.jNullReference; + final _$printWriter = printWriter.reference; + final _$strings = strings?.reference ?? jni$_.jNullReference; + _dump( + reference.pointer, + _id_dump as jni$_.JMethodIDPtr, + _$string.pointer, + _$fileDescriptor.pointer, + _$printWriter.pointer, + _$strings.pointer, + ).check(); + } + + static final _id_onAttachFragment = _class.instanceMethodId( + r'onAttachFragment', + r'(Landroidx/fragment/app/Fragment;)V', + ); + + static final _onAttachFragment = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onAttachFragment(androidx.fragment.app.Fragment fragment)` + void onAttachFragment(jni$_.JObject fragment) { + final _$fragment = fragment.reference; + _onAttachFragment( + reference.pointer, + _id_onAttachFragment as jni$_.JMethodIDPtr, + _$fragment.pointer, + ).check(); + } + + static final _id_getSupportFragmentManager = _class.instanceMethodId( + r'getSupportFragmentManager', + r'()Landroidx/fragment/app/FragmentManager;', + ); + + static final _getSupportFragmentManager = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public androidx.fragment.app.FragmentManager getSupportFragmentManager()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getSupportFragmentManager() { + return _getSupportFragmentManager( + reference.pointer, + _id_getSupportFragmentManager as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_getSupportLoaderManager = _class.instanceMethodId( + r'getSupportLoaderManager', + r'()Landroidx/loader/app/LoaderManager;', + ); + + static final _getSupportLoaderManager = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public androidx.loader.app.LoaderManager getSupportLoaderManager()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getSupportLoaderManager() { + return _getSupportLoaderManager( + reference.pointer, + _id_getSupportLoaderManager as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$Type$()); + } + + static final _id_validateRequestPermissionsRequestCode = _class + .instanceMethodId(r'validateRequestPermissionsRequestCode', r'(I)V'); + + static final _validateRequestPermissionsRequestCode = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public final void validateRequestPermissionsRequestCode(int i)` + void validateRequestPermissionsRequestCode(int i) { + _validateRequestPermissionsRequestCode( + reference.pointer, + _id_validateRequestPermissionsRequestCode as jni$_.JMethodIDPtr, + i, + ).check(); + } + + static final _id_onRequestPermissionsResult = _class.instanceMethodId( + r'onRequestPermissionsResult', + r'(I[Ljava/lang/String;[I)V', + ); + + static final _onRequestPermissionsResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Int32, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void onRequestPermissionsResult(int i, java.lang.String[] strings, int[] is)` + void onRequestPermissionsResult( + int i, + jni$_.JArray strings, + jni$_.JIntArray is$, + ) { + final _$strings = strings.reference; + final _$is$ = is$.reference; + _onRequestPermissionsResult( + reference.pointer, + _id_onRequestPermissionsResult as jni$_.JMethodIDPtr, + i, + _$strings.pointer, + _$is$.pointer, + ).check(); + } + + static final _id_startActivityFromFragment = _class.instanceMethodId( + r'startActivityFromFragment', + r'(Landroidx/fragment/app/Fragment;Landroid/content/Intent;I)V', + ); + + static final _startActivityFromFragment = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public void startActivityFromFragment(androidx.fragment.app.Fragment fragment, android.content.Intent intent, int i)` + void startActivityFromFragment(jni$_.JObject fragment, Intent intent, int i) { + final _$fragment = fragment.reference; + final _$intent = intent.reference; + _startActivityFromFragment( + reference.pointer, + _id_startActivityFromFragment as jni$_.JMethodIDPtr, + _$fragment.pointer, + _$intent.pointer, + i, + ).check(); + } + + static final _id_startActivityFromFragment$1 = _class.instanceMethodId( + r'startActivityFromFragment', + r'(Landroidx/fragment/app/Fragment;Landroid/content/Intent;ILandroid/os/Bundle;)V', + ); + + static final _startActivityFromFragment$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public void startActivityFromFragment(androidx.fragment.app.Fragment fragment, android.content.Intent intent, int i, android.os.Bundle bundle)` + void startActivityFromFragment$1( + jni$_.JObject fragment, + Intent intent, + int i, + jni$_.JObject? bundle, + ) { + final _$fragment = fragment.reference; + final _$intent = intent.reference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startActivityFromFragment$1( + reference.pointer, + _id_startActivityFromFragment$1 as jni$_.JMethodIDPtr, + _$fragment.pointer, + _$intent.pointer, + i, + _$bundle.pointer, + ).check(); + } + + static final _id_startIntentSenderFromFragment = _class.instanceMethodId( + r'startIntentSenderFromFragment', + r'(Landroidx/fragment/app/Fragment;Landroid/content/IntentSender;ILandroid/content/Intent;IIILandroid/os/Bundle;)V', + ); + + static final _startIntentSenderFromFragment = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + jni$_.Int32, + jni$_.Int32, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + jni$_.Pointer, + int, + int, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public void startIntentSenderFromFragment(androidx.fragment.app.Fragment fragment, android.content.IntentSender intentSender, int i, android.content.Intent intent, int i1, int i2, int i3, android.os.Bundle bundle)` + void startIntentSenderFromFragment( + jni$_.JObject fragment, + jni$_.JObject intentSender, + int i, + Intent? intent, + int i1, + int i2, + int i3, + jni$_.JObject? bundle, + ) { + final _$fragment = fragment.reference; + final _$intentSender = intentSender.reference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startIntentSenderFromFragment( + reference.pointer, + _id_startIntentSenderFromFragment as jni$_.JMethodIDPtr, + _$fragment.pointer, + _$intentSender.pointer, + i, + _$intent.pointer, + i1, + i2, + i3, + _$bundle.pointer, + ).check(); + } +} + +final class $FragmentActivity$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $FragmentActivity$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/fragment/app/FragmentActivity;'; + + @jni$_.internal + @core$_.override + FragmentActivity? fromReference(jni$_.JReference reference) => + reference.isNull ? null : FragmentActivity.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const $ComponentActivity$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 2; + + @core$_.override + int get hashCode => ($FragmentActivity$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($FragmentActivity$NullableType$) && + other is $FragmentActivity$NullableType$; + } +} + +final class $FragmentActivity$Type$ extends jni$_.JType { + @jni$_.internal + const $FragmentActivity$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/fragment/app/FragmentActivity;'; + + @jni$_.internal + @core$_.override + FragmentActivity fromReference(jni$_.JReference reference) => + FragmentActivity.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const $ComponentActivity$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $FragmentActivity$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 2; + + @core$_.override + int get hashCode => ($FragmentActivity$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($FragmentActivity$Type$) && + other is $FragmentActivity$Type$; + } +} + +/// from: `androidx.activity.result.ActivityResult$Companion` +class ActivityResult$Companion extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + ActivityResult$Companion.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/activity/result/ActivityResult$Companion', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $ActivityResult$Companion$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $ActivityResult$Companion$Type$(); + static final _id_resultCodeToString = _class.instanceMethodId( + r'resultCodeToString', + r'(I)Ljava/lang/String;', + ); + + static final _resultCodeToString = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public fun resultCodeToString(resultCode: kotlin.Int): kotlin.String` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString resultCodeToString(int i) { + return _resultCodeToString( + reference.pointer, + _id_resultCodeToString as jni$_.JMethodIDPtr, + i, + ).object(const jni$_.$JString$Type$()); + } + + static final _id_new$ = _class.constructorId( + r'(Lkotlin/jvm/internal/DefaultConstructorMarker;)V', + ); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `synthetic public void (kotlin.jvm.internal.DefaultConstructorMarker defaultConstructorMarker)` + /// The returned object must be released after use, by calling the [release] method. + factory ActivityResult$Companion(jni$_.JObject? defaultConstructorMarker) { + final _$defaultConstructorMarker = + defaultConstructorMarker?.reference ?? jni$_.jNullReference; + return ActivityResult$Companion.fromReference( + _new$( + _class.reference.pointer, + _id_new$ as jni$_.JMethodIDPtr, + _$defaultConstructorMarker.pointer, + ).reference, + ); + } +} + +final class $ActivityResult$Companion$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $ActivityResult$Companion$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/activity/result/ActivityResult$Companion;'; + + @jni$_.internal + @core$_.override + ActivityResult$Companion? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : ActivityResult$Companion.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ActivityResult$Companion$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityResult$Companion$NullableType$) && + other is $ActivityResult$Companion$NullableType$; + } +} + +final class $ActivityResult$Companion$Type$ + extends jni$_.JType { + @jni$_.internal + const $ActivityResult$Companion$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/activity/result/ActivityResult$Companion;'; + + @jni$_.internal + @core$_.override + ActivityResult$Companion fromReference(jni$_.JReference reference) => + ActivityResult$Companion.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $ActivityResult$Companion$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ActivityResult$Companion$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityResult$Companion$Type$) && + other is $ActivityResult$Companion$Type$; + } +} + +/// from: `androidx.activity.result.ActivityResult` +class ActivityResult extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + ActivityResult.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/activity/result/ActivityResult', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $ActivityResult$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $ActivityResult$Type$(); + static final _id_Companion = _class.staticFieldId( + r'Companion', + r'Landroidx/activity/result/ActivityResult$Companion;', + ); + + /// from: `static public final androidx.activity.result.ActivityResult$Companion Companion` + /// The returned object must be released after use, by calling the [release] method. + static ActivityResult$Companion get Companion => + _id_Companion.get(_class, const $ActivityResult$Companion$Type$()); + + static final _id_CREATOR = _class.staticFieldId( + r'CREATOR', + r'Landroid/os/Parcelable$Creator;', + ); + + /// from: `static public final android.os.Parcelable$Creator CREATOR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject get CREATOR => + _id_CREATOR.get(_class, const jni$_.$JObject$Type$()); + + static final _id_new$ = _class.constructorId(r'(ILandroid/content/Intent;)V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public void (int i, android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + factory ActivityResult(int i, Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return ActivityResult.fromReference( + _new$( + _class.reference.pointer, + _id_new$ as jni$_.JMethodIDPtr, + i, + _$intent.pointer, + ).reference, + ); + } + + static final _id_getResultCode = _class.instanceMethodId( + r'getResultCode', + r'()I', + ); + + static final _getResultCode = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public final int getResultCode()` + int getResultCode() { + return _getResultCode( + reference.pointer, + _id_getResultCode as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_getData = _class.instanceMethodId( + r'getData', + r'()Landroid/content/Intent;', + ); + + static final _getData = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public final android.content.Intent getData()` + /// The returned object must be released after use, by calling the [release] method. + Intent? getData() { + return _getData( + reference.pointer, + _id_getData as jni$_.JMethodIDPtr, + ).object(const $Intent$NullableType$()); + } + + static final _id_new$1 = _class.constructorId(r'(Landroid/os/Parcel;)V'); + + static final _new$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void (android.os.Parcel parcel)` + /// The returned object must be released after use, by calling the [release] method. + factory ActivityResult.new$1(jni$_.JObject parcel) { + final _$parcel = parcel.reference; + return ActivityResult.fromReference( + _new$1( + _class.reference.pointer, + _id_new$1 as jni$_.JMethodIDPtr, + _$parcel.pointer, + ).reference, + ); + } + + static final _id_toString$1 = _class.instanceMethodId( + r'toString', + r'()Ljava/lang/String;', + ); + + static final _toString$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun toString(): kotlin.String` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString toString$1() { + return _toString$1( + reference.pointer, + _id_toString$1 as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$Type$()); + } + + static final _id_writeToParcel = _class.instanceMethodId( + r'writeToParcel', + r'(Landroid/os/Parcel;I)V', + ); + + static final _writeToParcel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public fun writeToParcel(dest: android.os.Parcel, flags: kotlin.Int): kotlin.Unit` + void writeToParcel(jni$_.JObject parcel, int i) { + final _$parcel = parcel.reference; + _writeToParcel( + reference.pointer, + _id_writeToParcel as jni$_.JMethodIDPtr, + _$parcel.pointer, + i, + ).check(); + } + + static final _id_describeContents = _class.instanceMethodId( + r'describeContents', + r'()I', + ); + + static final _describeContents = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun describeContents(): kotlin.Int` + int describeContents() { + return _describeContents( + reference.pointer, + _id_describeContents as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_resultCodeToString = _class.staticMethodId( + r'resultCodeToString', + r'(I)Ljava/lang/String;', + ); + + static final _resultCodeToString = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `static public final java.lang.String resultCodeToString(int i)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString resultCodeToString(int i) { + return _resultCodeToString( + _class.reference.pointer, + _id_resultCodeToString as jni$_.JMethodIDPtr, + i, + ).object(const jni$_.$JString$Type$()); + } +} + +final class $ActivityResult$NullableType$ extends jni$_.JType { + @jni$_.internal + const $ActivityResult$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/activity/result/ActivityResult;'; + + @jni$_.internal + @core$_.override + ActivityResult? fromReference(jni$_.JReference reference) => + reference.isNull ? null : ActivityResult.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ActivityResult$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityResult$NullableType$) && + other is $ActivityResult$NullableType$; + } +} + +final class $ActivityResult$Type$ extends jni$_.JType { + @jni$_.internal + const $ActivityResult$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/activity/result/ActivityResult;'; + + @jni$_.internal + @core$_.override + ActivityResult fromReference(jni$_.JReference reference) => + ActivityResult.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $ActivityResult$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ActivityResult$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityResult$Type$) && + other is $ActivityResult$Type$; + } +} + +/// from: `androidx.core.app.ActivityCompat$OnRequestPermissionsResultCallback` +class ActivityCompat$OnRequestPermissionsResultCallback extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + ActivityCompat$OnRequestPermissionsResultCallback.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/core/app/ActivityCompat$OnRequestPermissionsResultCallback', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType + nullableType = + $ActivityCompat$OnRequestPermissionsResultCallback$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType + type = $ActivityCompat$OnRequestPermissionsResultCallback$Type$(); + static final _id_onRequestPermissionsResult = _class.instanceMethodId( + r'onRequestPermissionsResult', + r'(I[Ljava/lang/String;[I)V', + ); + + static final _onRequestPermissionsResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Int32, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onRequestPermissionsResult(int i, java.lang.String[] strings, int[] is)` + void onRequestPermissionsResult( + int i, + jni$_.JArray? strings, + jni$_.JIntArray? is$, + ) { + final _$strings = strings?.reference ?? jni$_.jNullReference; + final _$is$ = is$?.reference ?? jni$_.jNullReference; + _onRequestPermissionsResult( + reference.pointer, + _id_onRequestPermissionsResult as jni$_.JMethodIDPtr, + i, + _$strings.pointer, + _$is$.pointer, + ).check(); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map< + int, + $ActivityCompat$OnRequestPermissionsResultCallback + > + _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses(0, descriptor.address, args.address), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r'onRequestPermissionsResult(I[Ljava/lang/String;[I)V') { + _$impls[$p]!.onRequestPermissionsResult( + $a![0]! + .as(const jni$_.$JInteger$Type$(), releaseOriginal: true) + .intValue(releaseOriginal: true), + $a![1]?.as( + const jni$_.$JArray$Type$( + jni$_.$JString$NullableType$(), + ), + releaseOriginal: true, + ), + $a![2]?.as(const jni$_.$JIntArray$Type$(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $ActivityCompat$OnRequestPermissionsResultCallback $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'androidx.core.app.ActivityCompat$OnRequestPermissionsResultCallback', + $p, + _$invokePointer, + [ + if ($impl.onRequestPermissionsResult$async) + r'onRequestPermissionsResult(I[Ljava/lang/String;[I)V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory ActivityCompat$OnRequestPermissionsResultCallback.implement( + $ActivityCompat$OnRequestPermissionsResultCallback $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return ActivityCompat$OnRequestPermissionsResultCallback.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $ActivityCompat$OnRequestPermissionsResultCallback { + factory $ActivityCompat$OnRequestPermissionsResultCallback({ + required void Function( + int i, + jni$_.JArray? strings, + jni$_.JIntArray? is$, + ) + onRequestPermissionsResult, + bool onRequestPermissionsResult$async, + }) = _$ActivityCompat$OnRequestPermissionsResultCallback; + + void onRequestPermissionsResult( + int i, + jni$_.JArray? strings, + jni$_.JIntArray? is$, + ); + bool get onRequestPermissionsResult$async => false; +} + +final class _$ActivityCompat$OnRequestPermissionsResultCallback + with $ActivityCompat$OnRequestPermissionsResultCallback { + _$ActivityCompat$OnRequestPermissionsResultCallback({ + required void Function( + int i, + jni$_.JArray? strings, + jni$_.JIntArray? is$, + ) + onRequestPermissionsResult, + this.onRequestPermissionsResult$async = false, + }) : _onRequestPermissionsResult = onRequestPermissionsResult; + + final void Function( + int i, + jni$_.JArray? strings, + jni$_.JIntArray? is$, + ) + _onRequestPermissionsResult; + final bool onRequestPermissionsResult$async; + + void onRequestPermissionsResult( + int i, + jni$_.JArray? strings, + jni$_.JIntArray? is$, + ) { + return _onRequestPermissionsResult(i, strings, is$); + } +} + +final class $ActivityCompat$OnRequestPermissionsResultCallback$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $ActivityCompat$OnRequestPermissionsResultCallback$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/core/app/ActivityCompat$OnRequestPermissionsResultCallback;'; + + @jni$_.internal + @core$_.override + ActivityCompat$OnRequestPermissionsResultCallback? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : ActivityCompat$OnRequestPermissionsResultCallback.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType + get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($ActivityCompat$OnRequestPermissionsResultCallback$NullableType$) + .hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ActivityCompat$OnRequestPermissionsResultCallback$NullableType$) && + other + is $ActivityCompat$OnRequestPermissionsResultCallback$NullableType$; + } +} + +final class $ActivityCompat$OnRequestPermissionsResultCallback$Type$ + extends jni$_.JType { + @jni$_.internal + const $ActivityCompat$OnRequestPermissionsResultCallback$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/core/app/ActivityCompat$OnRequestPermissionsResultCallback;'; + + @jni$_.internal + @core$_.override + ActivityCompat$OnRequestPermissionsResultCallback fromReference( + jni$_.JReference reference, + ) => ActivityCompat$OnRequestPermissionsResultCallback.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType + get nullableType => + const $ActivityCompat$OnRequestPermissionsResultCallback$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($ActivityCompat$OnRequestPermissionsResultCallback$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ActivityCompat$OnRequestPermissionsResultCallback$Type$) && + other is $ActivityCompat$OnRequestPermissionsResultCallback$Type$; + } +} + +/// from: `androidx.core.app.ActivityCompat$PermissionCompatDelegate` +class ActivityCompat$PermissionCompatDelegate extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + ActivityCompat$PermissionCompatDelegate.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/core/app/ActivityCompat$PermissionCompatDelegate', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType + nullableType = $ActivityCompat$PermissionCompatDelegate$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $ActivityCompat$PermissionCompatDelegate$Type$(); + static final _id_requestPermissions = _class.instanceMethodId( + r'requestPermissions', + r'(Landroid/app/Activity;[Ljava/lang/String;I)Z', + ); + + static final _requestPermissions = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract boolean requestPermissions(android.app.Activity activity, java.lang.String[] strings, int i)` + bool requestPermissions( + jni$_.JObject? activity, + jni$_.JArray? strings, + int i, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$strings = strings?.reference ?? jni$_.jNullReference; + return _requestPermissions( + reference.pointer, + _id_requestPermissions as jni$_.JMethodIDPtr, + _$activity.pointer, + _$strings.pointer, + i, + ).boolean; + } + + static final _id_onActivityResult = _class.instanceMethodId( + r'onActivityResult', + r'(Landroid/app/Activity;IILandroid/content/Intent;)Z', + ); + + static final _onActivityResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract boolean onActivityResult(android.app.Activity activity, int i, int i1, android.content.Intent intent)` + bool onActivityResult( + jni$_.JObject? activity, + int i, + int i1, + Intent? intent, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _onActivityResult( + reference.pointer, + _id_onActivityResult as jni$_.JMethodIDPtr, + _$activity.pointer, + i, + i1, + _$intent.pointer, + ).boolean; + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map + _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses(0, descriptor.address, args.address), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == + r'requestPermissions(Landroid/app/Activity;[Ljava/lang/String;I)Z') { + final $r = _$impls[$p]!.requestPermissions( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + $a![1]?.as( + const jni$_.$JArray$Type$( + jni$_.$JString$NullableType$(), + ), + releaseOriginal: true, + ), + $a![2]! + .as(const jni$_.$JInteger$Type$(), releaseOriginal: true) + .intValue(releaseOriginal: true), + ); + return jni$_.JBoolean($r).reference.toPointer(); + } + if ($d == + r'onActivityResult(Landroid/app/Activity;IILandroid/content/Intent;)Z') { + final $r = _$impls[$p]!.onActivityResult( + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + $a![1]! + .as(const jni$_.$JInteger$Type$(), releaseOriginal: true) + .intValue(releaseOriginal: true), + $a![2]! + .as(const jni$_.$JInteger$Type$(), releaseOriginal: true) + .intValue(releaseOriginal: true), + $a![3]?.as(const $Intent$Type$(), releaseOriginal: true), + ); + return jni$_.JBoolean($r).reference.toPointer(); + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $ActivityCompat$PermissionCompatDelegate $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'androidx.core.app.ActivityCompat$PermissionCompatDelegate', + $p, + _$invokePointer, + [], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory ActivityCompat$PermissionCompatDelegate.implement( + $ActivityCompat$PermissionCompatDelegate $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return ActivityCompat$PermissionCompatDelegate.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $ActivityCompat$PermissionCompatDelegate { + factory $ActivityCompat$PermissionCompatDelegate({ + required bool Function( + jni$_.JObject? activity, + jni$_.JArray? strings, + int i, + ) + requestPermissions, + required bool Function( + jni$_.JObject? activity, + int i, + int i1, + Intent? intent, + ) + onActivityResult, + }) = _$ActivityCompat$PermissionCompatDelegate; + + bool requestPermissions( + jni$_.JObject? activity, + jni$_.JArray? strings, + int i, + ); + bool onActivityResult(jni$_.JObject? activity, int i, int i1, Intent? intent); +} + +final class _$ActivityCompat$PermissionCompatDelegate + with $ActivityCompat$PermissionCompatDelegate { + _$ActivityCompat$PermissionCompatDelegate({ + required bool Function( + jni$_.JObject? activity, + jni$_.JArray? strings, + int i, + ) + requestPermissions, + required bool Function( + jni$_.JObject? activity, + int i, + int i1, + Intent? intent, + ) + onActivityResult, + }) : _requestPermissions = requestPermissions, + _onActivityResult = onActivityResult; + + final bool Function( + jni$_.JObject? activity, + jni$_.JArray? strings, + int i, + ) + _requestPermissions; + final bool Function(jni$_.JObject? activity, int i, int i1, Intent? intent) + _onActivityResult; + + bool requestPermissions( + jni$_.JObject? activity, + jni$_.JArray? strings, + int i, + ) { + return _requestPermissions(activity, strings, i); + } + + bool onActivityResult( + jni$_.JObject? activity, + int i, + int i1, + Intent? intent, + ) { + return _onActivityResult(activity, i, i1, intent); + } +} + +final class $ActivityCompat$PermissionCompatDelegate$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $ActivityCompat$PermissionCompatDelegate$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/core/app/ActivityCompat$PermissionCompatDelegate;'; + + @jni$_.internal + @core$_.override + ActivityCompat$PermissionCompatDelegate? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : ActivityCompat$PermissionCompatDelegate.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($ActivityCompat$PermissionCompatDelegate$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ActivityCompat$PermissionCompatDelegate$NullableType$) && + other is $ActivityCompat$PermissionCompatDelegate$NullableType$; + } +} + +final class $ActivityCompat$PermissionCompatDelegate$Type$ + extends jni$_.JType { + @jni$_.internal + const $ActivityCompat$PermissionCompatDelegate$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/core/app/ActivityCompat$PermissionCompatDelegate;'; + + @jni$_.internal + @core$_.override + ActivityCompat$PermissionCompatDelegate fromReference( + jni$_.JReference reference, + ) => ActivityCompat$PermissionCompatDelegate.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $ActivityCompat$PermissionCompatDelegate$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ActivityCompat$PermissionCompatDelegate$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ActivityCompat$PermissionCompatDelegate$Type$) && + other is $ActivityCompat$PermissionCompatDelegate$Type$; + } +} + +/// from: `androidx.core.app.ActivityCompat$RequestPermissionsRequestCodeValidator` +class ActivityCompat$RequestPermissionsRequestCodeValidator + extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType + $type; + + @jni$_.internal + ActivityCompat$RequestPermissionsRequestCodeValidator.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/core/app/ActivityCompat$RequestPermissionsRequestCodeValidator', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType< + ActivityCompat$RequestPermissionsRequestCodeValidator? + > + nullableType = + $ActivityCompat$RequestPermissionsRequestCodeValidator$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType< + ActivityCompat$RequestPermissionsRequestCodeValidator + > + type = $ActivityCompat$RequestPermissionsRequestCodeValidator$Type$(); + static final _id_validateRequestPermissionsRequestCode = _class + .instanceMethodId(r'validateRequestPermissionsRequestCode', r'(I)V'); + + static final _validateRequestPermissionsRequestCode = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public abstract void validateRequestPermissionsRequestCode(int i)` + void validateRequestPermissionsRequestCode(int i) { + _validateRequestPermissionsRequestCode( + reference.pointer, + _id_validateRequestPermissionsRequestCode as jni$_.JMethodIDPtr, + i, + ).check(); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map< + int, + $ActivityCompat$RequestPermissionsRequestCodeValidator + > + _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses(0, descriptor.address, args.address), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r'validateRequestPermissionsRequestCode(I)V') { + _$impls[$p]!.validateRequestPermissionsRequestCode( + $a![0]! + .as(const jni$_.$JInteger$Type$(), releaseOriginal: true) + .intValue(releaseOriginal: true), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $ActivityCompat$RequestPermissionsRequestCodeValidator $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'androidx.core.app.ActivityCompat$RequestPermissionsRequestCodeValidator', + $p, + _$invokePointer, + [ + if ($impl.validateRequestPermissionsRequestCode$async) + r'validateRequestPermissionsRequestCode(I)V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory ActivityCompat$RequestPermissionsRequestCodeValidator.implement( + $ActivityCompat$RequestPermissionsRequestCodeValidator $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return ActivityCompat$RequestPermissionsRequestCodeValidator.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $ActivityCompat$RequestPermissionsRequestCodeValidator { + factory $ActivityCompat$RequestPermissionsRequestCodeValidator({ + required void Function(int i) validateRequestPermissionsRequestCode, + bool validateRequestPermissionsRequestCode$async, + }) = _$ActivityCompat$RequestPermissionsRequestCodeValidator; + + void validateRequestPermissionsRequestCode(int i); + bool get validateRequestPermissionsRequestCode$async => false; +} + +final class _$ActivityCompat$RequestPermissionsRequestCodeValidator + with $ActivityCompat$RequestPermissionsRequestCodeValidator { + _$ActivityCompat$RequestPermissionsRequestCodeValidator({ + required void Function(int i) validateRequestPermissionsRequestCode, + this.validateRequestPermissionsRequestCode$async = false, + }) : _validateRequestPermissionsRequestCode = + validateRequestPermissionsRequestCode; + + final void Function(int i) _validateRequestPermissionsRequestCode; + final bool validateRequestPermissionsRequestCode$async; + + void validateRequestPermissionsRequestCode(int i) { + return _validateRequestPermissionsRequestCode(i); + } +} + +final class $ActivityCompat$RequestPermissionsRequestCodeValidator$NullableType$ + extends + jni$_.JType { + @jni$_.internal + const $ActivityCompat$RequestPermissionsRequestCodeValidator$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/core/app/ActivityCompat$RequestPermissionsRequestCodeValidator;'; + + @jni$_.internal + @core$_.override + ActivityCompat$RequestPermissionsRequestCodeValidator? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : ActivityCompat$RequestPermissionsRequestCodeValidator.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType + get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($ActivityCompat$RequestPermissionsRequestCodeValidator$NullableType$) + .hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ActivityCompat$RequestPermissionsRequestCodeValidator$NullableType$) && + other + is $ActivityCompat$RequestPermissionsRequestCodeValidator$NullableType$; + } +} + +final class $ActivityCompat$RequestPermissionsRequestCodeValidator$Type$ + extends jni$_.JType { + @jni$_.internal + const $ActivityCompat$RequestPermissionsRequestCodeValidator$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/core/app/ActivityCompat$RequestPermissionsRequestCodeValidator;'; + + @jni$_.internal + @core$_.override + ActivityCompat$RequestPermissionsRequestCodeValidator fromReference( + jni$_.JReference reference, + ) => ActivityCompat$RequestPermissionsRequestCodeValidator.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType + get nullableType => + const $ActivityCompat$RequestPermissionsRequestCodeValidator$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($ActivityCompat$RequestPermissionsRequestCodeValidator$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ActivityCompat$RequestPermissionsRequestCodeValidator$Type$) && + other is $ActivityCompat$RequestPermissionsRequestCodeValidator$Type$; + } +} + +/// from: `androidx.core.app.ActivityCompat` +class ActivityCompat extends ContextCompat { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + ActivityCompat.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/core/app/ActivityCompat', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $ActivityCompat$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $ActivityCompat$Type$(); + static final _id_setPermissionCompatDelegate = _class.staticMethodId( + r'setPermissionCompatDelegate', + r'(Landroidx/core/app/ActivityCompat$PermissionCompatDelegate;)V', + ); + + static final _setPermissionCompatDelegate = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public void setPermissionCompatDelegate(androidx.core.app.ActivityCompat$PermissionCompatDelegate permissionCompatDelegate)` + static void setPermissionCompatDelegate( + ActivityCompat$PermissionCompatDelegate? permissionCompatDelegate, + ) { + final _$permissionCompatDelegate = + permissionCompatDelegate?.reference ?? jni$_.jNullReference; + _setPermissionCompatDelegate( + _class.reference.pointer, + _id_setPermissionCompatDelegate as jni$_.JMethodIDPtr, + _$permissionCompatDelegate.pointer, + ).check(); + } + + static final _id_getPermissionCompatDelegate = _class.staticMethodId( + r'getPermissionCompatDelegate', + r'()Landroidx/core/app/ActivityCompat$PermissionCompatDelegate;', + ); + + static final _getPermissionCompatDelegate = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `static public androidx.core.app.ActivityCompat$PermissionCompatDelegate getPermissionCompatDelegate()` + /// The returned object must be released after use, by calling the [release] method. + static ActivityCompat$PermissionCompatDelegate? + getPermissionCompatDelegate() { + return _getPermissionCompatDelegate( + _class.reference.pointer, + _id_getPermissionCompatDelegate as jni$_.JMethodIDPtr, + ).object( + const $ActivityCompat$PermissionCompatDelegate$NullableType$(), + ); + } + + static final _id_invalidateOptionsMenu = _class.staticMethodId( + r'invalidateOptionsMenu', + r'(Landroid/app/Activity;)Z', + ); + + static final _invalidateOptionsMenu = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public boolean invalidateOptionsMenu(android.app.Activity activity)` + static bool invalidateOptionsMenu(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + return _invalidateOptionsMenu( + _class.reference.pointer, + _id_invalidateOptionsMenu as jni$_.JMethodIDPtr, + _$activity.pointer, + ).boolean; + } + + static final _id_startActivityForResult = _class.staticMethodId( + r'startActivityForResult', + r'(Landroid/app/Activity;Landroid/content/Intent;ILandroid/os/Bundle;)V', + ); + + static final _startActivityForResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + jni$_.Pointer, + ) + >(); + + /// from: `static public void startActivityForResult(android.app.Activity activity, android.content.Intent intent, int i, android.os.Bundle bundle)` + static void startActivityForResult( + jni$_.JObject? activity, + Intent? intent, + int i, + jni$_.JObject? bundle, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startActivityForResult( + _class.reference.pointer, + _id_startActivityForResult as jni$_.JMethodIDPtr, + _$activity.pointer, + _$intent.pointer, + i, + _$bundle.pointer, + ).check(); + } + + static final _id_startIntentSenderForResult = _class.staticMethodId( + r'startIntentSenderForResult', + r'(Landroid/app/Activity;Landroid/content/IntentSender;ILandroid/content/Intent;IIILandroid/os/Bundle;)V', + ); + + static final _startIntentSenderForResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + jni$_.Int32, + jni$_.Int32, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + jni$_.Pointer, + int, + int, + int, + jni$_.Pointer, + ) + >(); + + /// from: `static public void startIntentSenderForResult(android.app.Activity activity, android.content.IntentSender intentSender, int i, android.content.Intent intent, int i1, int i2, int i3, android.os.Bundle bundle)` + static void startIntentSenderForResult( + jni$_.JObject? activity, + jni$_.JObject? intentSender, + int i, + Intent? intent, + int i1, + int i2, + int i3, + jni$_.JObject? bundle, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$intentSender = intentSender?.reference ?? jni$_.jNullReference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startIntentSenderForResult( + _class.reference.pointer, + _id_startIntentSenderForResult as jni$_.JMethodIDPtr, + _$activity.pointer, + _$intentSender.pointer, + i, + _$intent.pointer, + i1, + i2, + i3, + _$bundle.pointer, + ).check(); + } + + static final _id_finishAffinity = _class.staticMethodId( + r'finishAffinity', + r'(Landroid/app/Activity;)V', + ); + + static final _finishAffinity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public void finishAffinity(android.app.Activity activity)` + static void finishAffinity(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _finishAffinity( + _class.reference.pointer, + _id_finishAffinity as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_finishAfterTransition = _class.staticMethodId( + r'finishAfterTransition', + r'(Landroid/app/Activity;)V', + ); + + static final _finishAfterTransition = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public void finishAfterTransition(android.app.Activity activity)` + static void finishAfterTransition(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _finishAfterTransition( + _class.reference.pointer, + _id_finishAfterTransition as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_getReferrer = _class.staticMethodId( + r'getReferrer', + r'(Landroid/app/Activity;)Landroid/net/Uri;', + ); + + static final _getReferrer = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.net.Uri getReferrer(android.app.Activity activity)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getReferrer(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + return _getReferrer( + _class.reference.pointer, + _id_getReferrer as jni$_.JMethodIDPtr, + _$activity.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_requireViewById = _class.staticMethodId( + r'requireViewById', + r'(Landroid/app/Activity;I)Landroid/view/View;', + ); + + static final _requireViewById = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public T requireViewById(android.app.Activity activity, int i)` + /// The returned object must be released after use, by calling the [release] method. + static $T? requireViewById<$T extends jni$_.JObject?>( + jni$_.JObject? activity, + int i, { + required jni$_.JType<$T> T, + }) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + return _requireViewById( + _class.reference.pointer, + _id_requireViewById as jni$_.JMethodIDPtr, + _$activity.pointer, + i, + ).object<$T?>(T.nullableType); + } + + static final _id_setEnterSharedElementCallback = _class.staticMethodId( + r'setEnterSharedElementCallback', + r'(Landroid/app/Activity;Landroidx/core/app/SharedElementCallback;)V', + ); + + static final _setEnterSharedElementCallback = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public void setEnterSharedElementCallback(android.app.Activity activity, androidx.core.app.SharedElementCallback sharedElementCallback)` + static void setEnterSharedElementCallback( + jni$_.JObject? activity, + jni$_.JObject? sharedElementCallback, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$sharedElementCallback = + sharedElementCallback?.reference ?? jni$_.jNullReference; + _setEnterSharedElementCallback( + _class.reference.pointer, + _id_setEnterSharedElementCallback as jni$_.JMethodIDPtr, + _$activity.pointer, + _$sharedElementCallback.pointer, + ).check(); + } + + static final _id_setExitSharedElementCallback = _class.staticMethodId( + r'setExitSharedElementCallback', + r'(Landroid/app/Activity;Landroidx/core/app/SharedElementCallback;)V', + ); + + static final _setExitSharedElementCallback = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public void setExitSharedElementCallback(android.app.Activity activity, androidx.core.app.SharedElementCallback sharedElementCallback)` + static void setExitSharedElementCallback( + jni$_.JObject? activity, + jni$_.JObject? sharedElementCallback, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$sharedElementCallback = + sharedElementCallback?.reference ?? jni$_.jNullReference; + _setExitSharedElementCallback( + _class.reference.pointer, + _id_setExitSharedElementCallback as jni$_.JMethodIDPtr, + _$activity.pointer, + _$sharedElementCallback.pointer, + ).check(); + } + + static final _id_postponeEnterTransition = _class.staticMethodId( + r'postponeEnterTransition', + r'(Landroid/app/Activity;)V', + ); + + static final _postponeEnterTransition = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public void postponeEnterTransition(android.app.Activity activity)` + static void postponeEnterTransition(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _postponeEnterTransition( + _class.reference.pointer, + _id_postponeEnterTransition as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_startPostponedEnterTransition = _class.staticMethodId( + r'startPostponedEnterTransition', + r'(Landroid/app/Activity;)V', + ); + + static final _startPostponedEnterTransition = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public void startPostponedEnterTransition(android.app.Activity activity)` + static void startPostponedEnterTransition(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _startPostponedEnterTransition( + _class.reference.pointer, + _id_startPostponedEnterTransition as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_requestPermissions = _class.staticMethodId( + r'requestPermissions', + r'(Landroid/app/Activity;[Ljava/lang/String;I)V', + ); + + static final _requestPermissions = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public void requestPermissions(android.app.Activity activity, java.lang.String[] strings, int i)` + static void requestPermissions( + jni$_.JObject? activity, + jni$_.JArray? strings, + int i, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$strings = strings?.reference ?? jni$_.jNullReference; + _requestPermissions( + _class.reference.pointer, + _id_requestPermissions as jni$_.JMethodIDPtr, + _$activity.pointer, + _$strings.pointer, + i, + ).check(); + } + + static final _id_shouldShowRequestPermissionRationale = _class.staticMethodId( + r'shouldShowRequestPermissionRationale', + r'(Landroid/app/Activity;Ljava/lang/String;)Z', + ); + + static final _shouldShowRequestPermissionRationale = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public boolean shouldShowRequestPermissionRationale(android.app.Activity activity, java.lang.String string)` + static bool shouldShowRequestPermissionRationale( + jni$_.JObject? activity, + jni$_.JString? string, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _shouldShowRequestPermissionRationale( + _class.reference.pointer, + _id_shouldShowRequestPermissionRationale as jni$_.JMethodIDPtr, + _$activity.pointer, + _$string.pointer, + ).boolean; + } + + static final _id_isLaunchedFromBubble = _class.staticMethodId( + r'isLaunchedFromBubble', + r'(Landroid/app/Activity;)Z', + ); + + static final _isLaunchedFromBubble = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public boolean isLaunchedFromBubble(android.app.Activity activity)` + static bool isLaunchedFromBubble(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + return _isLaunchedFromBubble( + _class.reference.pointer, + _id_isLaunchedFromBubble as jni$_.JMethodIDPtr, + _$activity.pointer, + ).boolean; + } + + static final _id_requestDragAndDropPermissions = _class.staticMethodId( + r'requestDragAndDropPermissions', + r'(Landroid/app/Activity;Landroid/view/DragEvent;)Landroidx/core/view/DragAndDropPermissionsCompat;', + ); + + static final _requestDragAndDropPermissions = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public androidx.core.view.DragAndDropPermissionsCompat requestDragAndDropPermissions(android.app.Activity activity, android.view.DragEvent dragEvent)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? requestDragAndDropPermissions( + jni$_.JObject? activity, + jni$_.JObject? dragEvent, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$dragEvent = dragEvent?.reference ?? jni$_.jNullReference; + return _requestDragAndDropPermissions( + _class.reference.pointer, + _id_requestDragAndDropPermissions as jni$_.JMethodIDPtr, + _$activity.pointer, + _$dragEvent.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_recreate = _class.staticMethodId( + r'recreate', + r'(Landroid/app/Activity;)V', + ); + + static final _recreate = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public void recreate(android.app.Activity activity)` + static void recreate(jni$_.JObject? activity) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _recreate( + _class.reference.pointer, + _id_recreate as jni$_.JMethodIDPtr, + _$activity.pointer, + ).check(); + } + + static final _id_setLocusContext = _class.staticMethodId( + r'setLocusContext', + r'(Landroid/app/Activity;Landroidx/core/content/LocusIdCompat;Landroid/os/Bundle;)V', + ); + + static final _setLocusContext = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public void setLocusContext(android.app.Activity activity, androidx.core.content.LocusIdCompat locusIdCompat, android.os.Bundle bundle)` + static void setLocusContext( + jni$_.JObject? activity, + jni$_.JObject? locusIdCompat, + jni$_.JObject? bundle, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + final _$locusIdCompat = locusIdCompat?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _setLocusContext( + _class.reference.pointer, + _id_setLocusContext as jni$_.JMethodIDPtr, + _$activity.pointer, + _$locusIdCompat.pointer, + _$bundle.pointer, + ).check(); + } +} + +final class $ActivityCompat$NullableType$ extends jni$_.JType { + @jni$_.internal + const $ActivityCompat$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/core/app/ActivityCompat;'; + + @jni$_.internal + @core$_.override + ActivityCompat? fromReference(jni$_.JReference reference) => + reference.isNull ? null : ActivityCompat.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const $ContextCompat$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 2; + + @core$_.override + int get hashCode => ($ActivityCompat$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityCompat$NullableType$) && + other is $ActivityCompat$NullableType$; + } +} + +final class $ActivityCompat$Type$ extends jni$_.JType { + @jni$_.internal + const $ActivityCompat$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/core/app/ActivityCompat;'; + + @jni$_.internal + @core$_.override + ActivityCompat fromReference(jni$_.JReference reference) => + ActivityCompat.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const $ContextCompat$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $ActivityCompat$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 2; + + @core$_.override + int get hashCode => ($ActivityCompat$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityCompat$Type$) && + other is $ActivityCompat$Type$; + } +} + +/// from: `androidx.activity.result.ActivityResultCallback` +class ActivityResultCallback<$O extends jni$_.JObject?> extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType> $type; + + @jni$_.internal + final jni$_.JType<$O> O; + + @jni$_.internal + ActivityResultCallback.fromReference(this.O, jni$_.JReference reference) + : $type = type<$O>(O), + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/activity/result/ActivityResultCallback', + ); + + /// The type which includes information such as the signature of this class. + static jni$_.JType?> + nullableType<$O extends jni$_.JObject?>(jni$_.JType<$O> O) { + return $ActivityResultCallback$NullableType$<$O>(O); + } + + /// The type which includes information such as the signature of this class. + static jni$_.JType> + type<$O extends jni$_.JObject?>(jni$_.JType<$O> O) { + return $ActivityResultCallback$Type$<$O>(O); + } + + static final _id_onActivityResult = _class.instanceMethodId( + r'onActivityResult', + r'(Ljava/lang/Object;)V', + ); + + static final _onActivityResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun onActivityResult(result: O): kotlin.Unit` + void onActivityResult($O object) { + final _$object = object?.reference ?? jni$_.jNullReference; + _onActivityResult( + reference.pointer, + _id_onActivityResult as jni$_.JMethodIDPtr, + _$object.pointer, + ).check(); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses(0, descriptor.address, args.address), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r'onActivityResult(Ljava/lang/Object;)V') { + _$impls[$p]!.onActivityResult( + $a![0]?.as(_$impls[$p]!.O, releaseOriginal: true), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn<$O extends jni$_.JObject?>( + jni$_.JImplementer implementer, + $ActivityResultCallback<$O> $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'androidx.activity.result.ActivityResultCallback', + $p, + _$invokePointer, + [ + if ($impl.onActivityResult$async) + r'onActivityResult(Ljava/lang/Object;)V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory ActivityResultCallback.implement($ActivityResultCallback<$O> $impl) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return ActivityResultCallback<$O>.fromReference( + $impl.O, + $i.implementReference(), + ); + } +} + +abstract base mixin class $ActivityResultCallback<$O extends jni$_.JObject?> { + factory $ActivityResultCallback({ + required jni$_.JType<$O> O, + required void Function($O object) onActivityResult, + bool onActivityResult$async, + }) = _$ActivityResultCallback<$O>; + + jni$_.JType<$O> get O; + + void onActivityResult($O object); + bool get onActivityResult$async => false; +} + +final class _$ActivityResultCallback<$O extends jni$_.JObject?> + with $ActivityResultCallback<$O> { + _$ActivityResultCallback({ + required this.O, + required void Function($O object) onActivityResult, + this.onActivityResult$async = false, + }) : _onActivityResult = onActivityResult; + + @core$_.override + final jni$_.JType<$O> O; + + final void Function($O object) _onActivityResult; + final bool onActivityResult$async; + + void onActivityResult($O object) { + return _onActivityResult(object); + } +} + +final class $ActivityResultCallback$NullableType$<$O extends jni$_.JObject?> + extends jni$_.JType?> { + @jni$_.internal + final jni$_.JType<$O> O; + + @jni$_.internal + const $ActivityResultCallback$NullableType$(this.O); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/activity/result/ActivityResultCallback;'; + + @jni$_.internal + @core$_.override + ActivityResultCallback<$O>? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : ActivityResultCallback<$O>.fromReference(O, reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType?> get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($ActivityResultCallback$NullableType$, O); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityResultCallback$NullableType$<$O>) && + other is $ActivityResultCallback$NullableType$<$O> && + O == other.O; + } +} + +final class $ActivityResultCallback$Type$<$O extends jni$_.JObject?> + extends jni$_.JType> { + @jni$_.internal + final jni$_.JType<$O> O; + + @jni$_.internal + const $ActivityResultCallback$Type$(this.O); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/activity/result/ActivityResultCallback;'; + + @jni$_.internal + @core$_.override + ActivityResultCallback<$O> fromReference(jni$_.JReference reference) => + ActivityResultCallback<$O>.fromReference(O, reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType?> get nullableType => + $ActivityResultCallback$NullableType$<$O>(O); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($ActivityResultCallback$Type$, O); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityResultCallback$Type$<$O>) && + other is $ActivityResultCallback$Type$<$O> && + O == other.O; + } +} + +/// from: `androidx.activity.result.ActivityResultLauncher` +class ActivityResultLauncher<$I extends jni$_.JObject?> extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType> $type; + + @jni$_.internal + final jni$_.JType<$I> I; + + @jni$_.internal + ActivityResultLauncher.fromReference(this.I, jni$_.JReference reference) + : $type = type<$I>(I), + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/activity/result/ActivityResultLauncher', + ); + + /// The type which includes information such as the signature of this class. + static jni$_.JType?> + nullableType<$I extends jni$_.JObject?>(jni$_.JType<$I> I) { + return $ActivityResultLauncher$NullableType$<$I>(I); + } + + /// The type which includes information such as the signature of this class. + static jni$_.JType> + type<$I extends jni$_.JObject?>(jni$_.JType<$I> I) { + return $ActivityResultLauncher$Type$<$I>(I); + } + + static final _id_launch = _class.instanceMethodId( + r'launch', + r'(Ljava/lang/Object;)V', + ); + + static final _launch = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public fun launch(input: I): kotlin.Unit` + void launch($I object) { + final _$object = object?.reference ?? jni$_.jNullReference; + _launch( + reference.pointer, + _id_launch as jni$_.JMethodIDPtr, + _$object.pointer, + ).check(); + } + + static final _id_launch$1 = _class.instanceMethodId( + r'launch', + r'(Ljava/lang/Object;Landroidx/core/app/ActivityOptionsCompat;)V', + ); + + static final _launch$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun launch(input: I, options: androidx.core.app.ActivityOptionsCompat?): kotlin.Unit` + void launch$1($I object, jni$_.JObject? activityOptionsCompat) { + final _$object = object?.reference ?? jni$_.jNullReference; + final _$activityOptionsCompat = + activityOptionsCompat?.reference ?? jni$_.jNullReference; + _launch$1( + reference.pointer, + _id_launch$1 as jni$_.JMethodIDPtr, + _$object.pointer, + _$activityOptionsCompat.pointer, + ).check(); + } + + static final _id_unregister = _class.instanceMethodId(r'unregister', r'()V'); + + static final _unregister = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public fun unregister(): kotlin.Unit` + void unregister() { + _unregister( + reference.pointer, + _id_unregister as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_getContract = _class.instanceMethodId( + r'getContract', + r'()Landroidx/activity/result/contract/ActivityResultContract;', + ); + + static final _getContract = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract androidx.activity.result.contract.ActivityResultContract getContract()` + /// The returned object must be released after use, by calling the [release] method. + ActivityResultContract<$I, jni$_.JObject?> getContract() { + return _getContract( + reference.pointer, + _id_getContract as jni$_.JMethodIDPtr, + ).object>( + $ActivityResultContract$Type$<$I, jni$_.JObject?>( + I, + const jni$_.$JObject$NullableType$(), + ), + ); + } +} + +final class $ActivityResultLauncher$NullableType$<$I extends jni$_.JObject?> + extends jni$_.JType?> { + @jni$_.internal + final jni$_.JType<$I> I; + + @jni$_.internal + const $ActivityResultLauncher$NullableType$(this.I); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/activity/result/ActivityResultLauncher;'; + + @jni$_.internal + @core$_.override + ActivityResultLauncher<$I>? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : ActivityResultLauncher<$I>.fromReference(I, reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType?> get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($ActivityResultLauncher$NullableType$, I); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityResultLauncher$NullableType$<$I>) && + other is $ActivityResultLauncher$NullableType$<$I> && + I == other.I; + } +} + +final class $ActivityResultLauncher$Type$<$I extends jni$_.JObject?> + extends jni$_.JType> { + @jni$_.internal + final jni$_.JType<$I> I; + + @jni$_.internal + const $ActivityResultLauncher$Type$(this.I); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/activity/result/ActivityResultLauncher;'; + + @jni$_.internal + @core$_.override + ActivityResultLauncher<$I> fromReference(jni$_.JReference reference) => + ActivityResultLauncher<$I>.fromReference(I, reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType?> get nullableType => + $ActivityResultLauncher$NullableType$<$I>(I); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($ActivityResultLauncher$Type$, I); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityResultLauncher$Type$<$I>) && + other is $ActivityResultLauncher$Type$<$I> && + I == other.I; + } +} + +/// from: `androidx.activity.result.contract.ActivityResultContract$SynchronousResult` +class ActivityResultContract$SynchronousResult<$T extends jni$_.JObject?> + extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType> $type; + + @jni$_.internal + final jni$_.JType<$T> T; + + @jni$_.internal + ActivityResultContract$SynchronousResult.fromReference( + this.T, + jni$_.JReference reference, + ) : $type = type<$T>(T), + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/activity/result/contract/ActivityResultContract$SynchronousResult', + ); + + /// The type which includes information such as the signature of this class. + static jni$_.JType?> + nullableType<$T extends jni$_.JObject?>(jni$_.JType<$T> T) { + return $ActivityResultContract$SynchronousResult$NullableType$<$T>(T); + } + + /// The type which includes information such as the signature of this class. + static jni$_.JType> + type<$T extends jni$_.JObject?>(jni$_.JType<$T> T) { + return $ActivityResultContract$SynchronousResult$Type$<$T>(T); + } + + static final _id_new$ = _class.constructorId(r'(Ljava/lang/Object;)V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void (T object)` + /// The returned object must be released after use, by calling the [release] method. + factory ActivityResultContract$SynchronousResult( + $T object, { + required jni$_.JType<$T> T, + }) { + final _$object = object?.reference ?? jni$_.jNullReference; + return ActivityResultContract$SynchronousResult<$T>.fromReference( + T, + _new$( + _class.reference.pointer, + _id_new$ as jni$_.JMethodIDPtr, + _$object.pointer, + ).reference, + ); + } + + static final _id_getValue = _class.instanceMethodId( + r'getValue', + r'()Ljava/lang/Object;', + ); + + static final _getValue = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public final T getValue()` + /// The returned object must be released after use, by calling the [release] method. + $T getValue() { + return _getValue( + reference.pointer, + _id_getValue as jni$_.JMethodIDPtr, + ).object<$T>(T); + } +} + +final class $ActivityResultContract$SynchronousResult$NullableType$< + $T extends jni$_.JObject? +> + extends jni$_.JType?> { + @jni$_.internal + final jni$_.JType<$T> T; + + @jni$_.internal + const $ActivityResultContract$SynchronousResult$NullableType$(this.T); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/activity/result/contract/ActivityResultContract$SynchronousResult;'; + + @jni$_.internal + @core$_.override + ActivityResultContract$SynchronousResult<$T>? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : ActivityResultContract$SynchronousResult<$T>.fromReference( + T, + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType?> get nullableType => + this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + Object.hash($ActivityResultContract$SynchronousResult$NullableType$, T); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ActivityResultContract$SynchronousResult$NullableType$<$T>) && + other is $ActivityResultContract$SynchronousResult$NullableType$<$T> && + T == other.T; + } +} + +final class $ActivityResultContract$SynchronousResult$Type$< + $T extends jni$_.JObject? +> + extends jni$_.JType> { + @jni$_.internal + final jni$_.JType<$T> T; + + @jni$_.internal + const $ActivityResultContract$SynchronousResult$Type$(this.T); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/activity/result/contract/ActivityResultContract$SynchronousResult;'; + + @jni$_.internal + @core$_.override + ActivityResultContract$SynchronousResult<$T> fromReference( + jni$_.JReference reference, + ) => ActivityResultContract$SynchronousResult<$T>.fromReference(T, reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType?> get nullableType => + $ActivityResultContract$SynchronousResult$NullableType$<$T>(T); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + Object.hash($ActivityResultContract$SynchronousResult$Type$, T); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ActivityResultContract$SynchronousResult$Type$<$T>) && + other is $ActivityResultContract$SynchronousResult$Type$<$T> && + T == other.T; + } +} + +/// from: `androidx.activity.result.contract.ActivityResultContract` +class ActivityResultContract< + $I extends jni$_.JObject?, + $O extends jni$_.JObject? +> + extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType> $type; + + @jni$_.internal + final jni$_.JType<$I> I; + + @jni$_.internal + final jni$_.JType<$O> O; + + @jni$_.internal + ActivityResultContract.fromReference( + this.I, + this.O, + jni$_.JReference reference, + ) : $type = type<$I, $O>(I, O), + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/activity/result/contract/ActivityResultContract', + ); + + /// The type which includes information such as the signature of this class. + static jni$_.JType?> nullableType< + $I extends jni$_.JObject?, + $O extends jni$_.JObject? + >(jni$_.JType<$I> I, jni$_.JType<$O> O) { + return $ActivityResultContract$NullableType$<$I, $O>(I, O); + } + + /// The type which includes information such as the signature of this class. + static jni$_.JType> type< + $I extends jni$_.JObject?, + $O extends jni$_.JObject? + >(jni$_.JType<$I> I, jni$_.JType<$O> O) { + return $ActivityResultContract$Type$<$I, $O>(I, O); + } + + static final _id_createIntent = _class.instanceMethodId( + r'createIntent', + r'(Landroid/content/Context;Ljava/lang/Object;)Landroid/content/Intent;', + ); + + static final _createIntent = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun createIntent(context: android.content.Context, input: I): android.content.Intent` + /// The returned object must be released after use, by calling the [release] method. + Intent createIntent(jni$_.JObject context, $I object) { + final _$context = context.reference; + final _$object = object?.reference ?? jni$_.jNullReference; + return _createIntent( + reference.pointer, + _id_createIntent as jni$_.JMethodIDPtr, + _$context.pointer, + _$object.pointer, + ).object(const $Intent$Type$()); + } + + static final _id_parseResult = _class.instanceMethodId( + r'parseResult', + r'(ILandroid/content/Intent;)Ljava/lang/Object;', + ); + + static final _parseResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Pointer)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public fun parseResult(resultCode: kotlin.Int, intent: android.content.Intent?): O` + /// The returned object must be released after use, by calling the [release] method. + $O parseResult(int i, Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _parseResult( + reference.pointer, + _id_parseResult as jni$_.JMethodIDPtr, + i, + _$intent.pointer, + ).object<$O>(O); + } + + static final _id_getSynchronousResult = _class.instanceMethodId( + r'getSynchronousResult', + r'(Landroid/content/Context;Ljava/lang/Object;)Landroidx/activity/result/contract/ActivityResultContract$SynchronousResult;', + ); + + static final _getSynchronousResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public fun getSynchronousResult(context: android.content.Context, input: I): androidx.activity.result.contract.ActivityResultContract.SynchronousResult?` + /// The returned object must be released after use, by calling the [release] method. + ActivityResultContract$SynchronousResult<$O>? getSynchronousResult( + jni$_.JObject context, + $I object, + ) { + final _$context = context.reference; + final _$object = object?.reference ?? jni$_.jNullReference; + return _getSynchronousResult( + reference.pointer, + _id_getSynchronousResult as jni$_.JMethodIDPtr, + _$context.pointer, + _$object.pointer, + ).object?>( + $ActivityResultContract$SynchronousResult$NullableType$<$O>(O), + ); + } +} + +final class $ActivityResultContract$NullableType$< + $I extends jni$_.JObject?, + $O extends jni$_.JObject? +> + extends jni$_.JType?> { + @jni$_.internal + final jni$_.JType<$I> I; + + @jni$_.internal + final jni$_.JType<$O> O; + + @jni$_.internal + const $ActivityResultContract$NullableType$(this.I, this.O); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/activity/result/contract/ActivityResultContract;'; + + @jni$_.internal + @core$_.override + ActivityResultContract<$I, $O>? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : ActivityResultContract<$I, $O>.fromReference(I, O, reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType?> get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($ActivityResultContract$NullableType$, I, O); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ActivityResultContract$NullableType$<$I, $O>) && + other is $ActivityResultContract$NullableType$<$I, $O> && + I == other.I && + O == other.O; + } +} + +final class $ActivityResultContract$Type$< + $I extends jni$_.JObject?, + $O extends jni$_.JObject? +> + extends jni$_.JType> { + @jni$_.internal + final jni$_.JType<$I> I; + + @jni$_.internal + final jni$_.JType<$O> O; + + @jni$_.internal + const $ActivityResultContract$Type$(this.I, this.O); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/activity/result/contract/ActivityResultContract;'; + + @jni$_.internal + @core$_.override + ActivityResultContract<$I, $O> fromReference(jni$_.JReference reference) => + ActivityResultContract<$I, $O>.fromReference(I, O, reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType?> get nullableType => + $ActivityResultContract$NullableType$<$I, $O>(I, O); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($ActivityResultContract$Type$, I, O); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ActivityResultContract$Type$<$I, $O>) && + other is $ActivityResultContract$Type$<$I, $O> && + I == other.I && + O == other.O; + } +} + +/// from: `android.content.Intent$FilterComparison` +class Intent$FilterComparison extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Intent$FilterComparison.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/Intent$FilterComparison', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $Intent$FilterComparison$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $Intent$FilterComparison$Type$(); + static final _id_new$ = _class.constructorId(r'(Landroid/content/Intent;)V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void (android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent$FilterComparison(Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return Intent$FilterComparison.fromReference( + _new$( + _class.reference.pointer, + _id_new$ as jni$_.JMethodIDPtr, + _$intent.pointer, + ).reference, + ); + } + + static final _id_equals = _class.instanceMethodId( + r'equals', + r'(Ljava/lang/Object;)Z', + ); + + static final _equals = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean equals(java.lang.Object object)` + bool equals(jni$_.JObject? object) { + final _$object = object?.reference ?? jni$_.jNullReference; + return _equals( + reference.pointer, + _id_equals as jni$_.JMethodIDPtr, + _$object.pointer, + ).boolean; + } + + static final _id_getIntent = _class.instanceMethodId( + r'getIntent', + r'()Landroid/content/Intent;', + ); + + static final _getIntent = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.content.Intent getIntent()` + /// The returned object must be released after use, by calling the [release] method. + Intent? getIntent() { + return _getIntent( + reference.pointer, + _id_getIntent as jni$_.JMethodIDPtr, + ).object(const $Intent$NullableType$()); + } + + static final _id_hashCode$1 = _class.instanceMethodId(r'hashCode', r'()I'); + + static final _hashCode$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int hashCode()` + int hashCode$1() { + return _hashCode$1( + reference.pointer, + _id_hashCode$1 as jni$_.JMethodIDPtr, + ).integer; + } +} + +final class $Intent$FilterComparison$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $Intent$FilterComparison$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent$FilterComparison;'; + + @jni$_.internal + @core$_.override + Intent$FilterComparison? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : Intent$FilterComparison.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$FilterComparison$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$FilterComparison$NullableType$) && + other is $Intent$FilterComparison$NullableType$; + } +} + +final class $Intent$FilterComparison$Type$ + extends jni$_.JType { + @jni$_.internal + const $Intent$FilterComparison$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent$FilterComparison;'; + + @jni$_.internal + @core$_.override + Intent$FilterComparison fromReference(jni$_.JReference reference) => + Intent$FilterComparison.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $Intent$FilterComparison$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$FilterComparison$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$FilterComparison$Type$) && + other is $Intent$FilterComparison$Type$; + } +} + +/// from: `android.content.Intent$ShortcutIconResource` +class Intent$ShortcutIconResource extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Intent$ShortcutIconResource.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/Intent$ShortcutIconResource', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $Intent$ShortcutIconResource$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $Intent$ShortcutIconResource$Type$(); + static final _id_CREATOR = _class.staticFieldId( + r'CREATOR', + r'Landroid/os/Parcelable$Creator;', + ); + + /// from: `static public final android.os.Parcelable$Creator CREATOR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? get CREATOR => + _id_CREATOR.get(_class, const jni$_.$JObject$NullableType$()); + + static final _id_packageName = _class.instanceFieldId( + r'packageName', + r'Ljava/lang/String;', + ); + + /// from: `public java.lang.String packageName` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? get packageName => + _id_packageName.get(this, const jni$_.$JString$NullableType$()); + + /// from: `public java.lang.String packageName` + /// The returned object must be released after use, by calling the [release] method. + set packageName(jni$_.JString? value) => + _id_packageName.set(this, const jni$_.$JString$NullableType$(), value); + + static final _id_resourceName = _class.instanceFieldId( + r'resourceName', + r'Ljava/lang/String;', + ); + + /// from: `public java.lang.String resourceName` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? get resourceName => + _id_resourceName.get(this, const jni$_.$JString$NullableType$()); + + /// from: `public java.lang.String resourceName` + /// The returned object must be released after use, by calling the [release] method. + set resourceName(jni$_.JString? value) => + _id_resourceName.set(this, const jni$_.$JString$NullableType$(), value); + + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory Intent$ShortcutIconResource() { + return Intent$ShortcutIconResource.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } + + static final _id_describeContents = _class.instanceMethodId( + r'describeContents', + r'()I', + ); + + static final _describeContents = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int describeContents()` + int describeContents() { + return _describeContents( + reference.pointer, + _id_describeContents as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_fromContext = _class.staticMethodId( + r'fromContext', + r'(Landroid/content/Context;I)Landroid/content/Intent$ShortcutIconResource;', + ); + + static final _fromContext = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public android.content.Intent$ShortcutIconResource fromContext(android.content.Context context, int i)` + /// The returned object must be released after use, by calling the [release] method. + static Intent$ShortcutIconResource? fromContext( + jni$_.JObject? context, + int i, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _fromContext( + _class.reference.pointer, + _id_fromContext as jni$_.JMethodIDPtr, + _$context.pointer, + i, + ).object( + const $Intent$ShortcutIconResource$NullableType$(), + ); + } + + static final _id_toString$1 = _class.instanceMethodId( + r'toString', + r'()Ljava/lang/String;', + ); + + static final _toString$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String toString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toString$1() { + return _toString$1( + reference.pointer, + _id_toString$1 as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_writeToParcel = _class.instanceMethodId( + r'writeToParcel', + r'(Landroid/os/Parcel;I)V', + ); + + static final _writeToParcel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public void writeToParcel(android.os.Parcel parcel, int i)` + void writeToParcel(jni$_.JObject? parcel, int i) { + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _writeToParcel( + reference.pointer, + _id_writeToParcel as jni$_.JMethodIDPtr, + _$parcel.pointer, + i, + ).check(); + } +} + +final class $Intent$ShortcutIconResource$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $Intent$ShortcutIconResource$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent$ShortcutIconResource;'; + + @jni$_.internal + @core$_.override + Intent$ShortcutIconResource? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : Intent$ShortcutIconResource.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$ShortcutIconResource$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$ShortcutIconResource$NullableType$) && + other is $Intent$ShortcutIconResource$NullableType$; + } +} + +final class $Intent$ShortcutIconResource$Type$ + extends jni$_.JType { + @jni$_.internal + const $Intent$ShortcutIconResource$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent$ShortcutIconResource;'; + + @jni$_.internal + @core$_.override + Intent$ShortcutIconResource fromReference(jni$_.JReference reference) => + Intent$ShortcutIconResource.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $Intent$ShortcutIconResource$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$ShortcutIconResource$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$ShortcutIconResource$Type$) && + other is $Intent$ShortcutIconResource$Type$; + } +} + +/// from: `android.content.Intent` +class Intent extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Intent.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName(r'android/content/Intent'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Intent$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Intent$Type$(); + static final _id_ACTION_AIRPLANE_MODE_CHANGED = _class.staticFieldId( + r'ACTION_AIRPLANE_MODE_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_AIRPLANE_MODE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_AIRPLANE_MODE_CHANGED => + _id_ACTION_AIRPLANE_MODE_CHANGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_ALL_APPS = _class.staticFieldId( + r'ACTION_ALL_APPS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_ALL_APPS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_ALL_APPS => + _id_ACTION_ALL_APPS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_ANSWER = _class.staticFieldId( + r'ACTION_ANSWER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_ANSWER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_ANSWER => + _id_ACTION_ANSWER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_APPLICATION_LOCALE_CHANGED = _class.staticFieldId( + r'ACTION_APPLICATION_LOCALE_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_APPLICATION_LOCALE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_APPLICATION_LOCALE_CHANGED => + _id_ACTION_APPLICATION_LOCALE_CHANGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_APPLICATION_PREFERENCES = _class.staticFieldId( + r'ACTION_APPLICATION_PREFERENCES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_APPLICATION_PREFERENCES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_APPLICATION_PREFERENCES => + _id_ACTION_APPLICATION_PREFERENCES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_APPLICATION_RESTRICTIONS_CHANGED = _class + .staticFieldId( + r'ACTION_APPLICATION_RESTRICTIONS_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_APPLICATION_RESTRICTIONS_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_APPLICATION_RESTRICTIONS_CHANGED => + _id_ACTION_APPLICATION_RESTRICTIONS_CHANGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_APP_ERROR = _class.staticFieldId( + r'ACTION_APP_ERROR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_APP_ERROR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_APP_ERROR => + _id_ACTION_APP_ERROR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_ASSIST = _class.staticFieldId( + r'ACTION_ASSIST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_ASSIST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_ASSIST => + _id_ACTION_ASSIST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_ATTACH_DATA = _class.staticFieldId( + r'ACTION_ATTACH_DATA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_ATTACH_DATA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_ATTACH_DATA => + _id_ACTION_ATTACH_DATA.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_AUTO_REVOKE_PERMISSIONS = _class.staticFieldId( + r'ACTION_AUTO_REVOKE_PERMISSIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_AUTO_REVOKE_PERMISSIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_AUTO_REVOKE_PERMISSIONS => + _id_ACTION_AUTO_REVOKE_PERMISSIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_BATTERY_CHANGED = _class.staticFieldId( + r'ACTION_BATTERY_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_BATTERY_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_BATTERY_CHANGED => _id_ACTION_BATTERY_CHANGED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_BATTERY_LOW = _class.staticFieldId( + r'ACTION_BATTERY_LOW', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_BATTERY_LOW` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_BATTERY_LOW => + _id_ACTION_BATTERY_LOW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_BATTERY_OKAY = _class.staticFieldId( + r'ACTION_BATTERY_OKAY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_BATTERY_OKAY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_BATTERY_OKAY => + _id_ACTION_BATTERY_OKAY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_BOOT_COMPLETED = _class.staticFieldId( + r'ACTION_BOOT_COMPLETED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_BOOT_COMPLETED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_BOOT_COMPLETED => _id_ACTION_BOOT_COMPLETED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_BUG_REPORT = _class.staticFieldId( + r'ACTION_BUG_REPORT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_BUG_REPORT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_BUG_REPORT => + _id_ACTION_BUG_REPORT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CALL = _class.staticFieldId( + r'ACTION_CALL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CALL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CALL => + _id_ACTION_CALL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CALL_BUTTON = _class.staticFieldId( + r'ACTION_CALL_BUTTON', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CALL_BUTTON` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CALL_BUTTON => + _id_ACTION_CALL_BUTTON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CAMERA_BUTTON = _class.staticFieldId( + r'ACTION_CAMERA_BUTTON', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CAMERA_BUTTON` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CAMERA_BUTTON => _id_ACTION_CAMERA_BUTTON + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CARRIER_SETUP = _class.staticFieldId( + r'ACTION_CARRIER_SETUP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CARRIER_SETUP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CARRIER_SETUP => _id_ACTION_CARRIER_SETUP + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CHOOSER = _class.staticFieldId( + r'ACTION_CHOOSER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CHOOSER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CHOOSER => + _id_ACTION_CHOOSER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CLOSE_SYSTEM_DIALOGS = _class.staticFieldId( + r'ACTION_CLOSE_SYSTEM_DIALOGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CLOSE_SYSTEM_DIALOGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CLOSE_SYSTEM_DIALOGS => + _id_ACTION_CLOSE_SYSTEM_DIALOGS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_CONFIGURATION_CHANGED = _class.staticFieldId( + r'ACTION_CONFIGURATION_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CONFIGURATION_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CONFIGURATION_CHANGED => + _id_ACTION_CONFIGURATION_CHANGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_CREATE_DOCUMENT = _class.staticFieldId( + r'ACTION_CREATE_DOCUMENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CREATE_DOCUMENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CREATE_DOCUMENT => _id_ACTION_CREATE_DOCUMENT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CREATE_NOTE = _class.staticFieldId( + r'ACTION_CREATE_NOTE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CREATE_NOTE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CREATE_NOTE => + _id_ACTION_CREATE_NOTE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CREATE_REMINDER = _class.staticFieldId( + r'ACTION_CREATE_REMINDER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CREATE_REMINDER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CREATE_REMINDER => _id_ACTION_CREATE_REMINDER + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_CREATE_SHORTCUT = _class.staticFieldId( + r'ACTION_CREATE_SHORTCUT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_CREATE_SHORTCUT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_CREATE_SHORTCUT => _id_ACTION_CREATE_SHORTCUT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DATE_CHANGED = _class.staticFieldId( + r'ACTION_DATE_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DATE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DATE_CHANGED => + _id_ACTION_DATE_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DEFAULT = _class.staticFieldId( + r'ACTION_DEFAULT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DEFAULT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DEFAULT => + _id_ACTION_DEFAULT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DEFINE = _class.staticFieldId( + r'ACTION_DEFINE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DEFINE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DEFINE => + _id_ACTION_DEFINE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DELETE = _class.staticFieldId( + r'ACTION_DELETE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DELETE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DELETE => + _id_ACTION_DELETE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DEVICE_STORAGE_LOW = _class.staticFieldId( + r'ACTION_DEVICE_STORAGE_LOW', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DEVICE_STORAGE_LOW` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DEVICE_STORAGE_LOW => + _id_ACTION_DEVICE_STORAGE_LOW.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_DEVICE_STORAGE_OK = _class.staticFieldId( + r'ACTION_DEVICE_STORAGE_OK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DEVICE_STORAGE_OK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DEVICE_STORAGE_OK => + _id_ACTION_DEVICE_STORAGE_OK.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_DIAL = _class.staticFieldId( + r'ACTION_DIAL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DIAL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DIAL => + _id_ACTION_DIAL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DOCK_EVENT = _class.staticFieldId( + r'ACTION_DOCK_EVENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DOCK_EVENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DOCK_EVENT => + _id_ACTION_DOCK_EVENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_DREAMING_STARTED = _class.staticFieldId( + r'ACTION_DREAMING_STARTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DREAMING_STARTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DREAMING_STARTED => + _id_ACTION_DREAMING_STARTED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_DREAMING_STOPPED = _class.staticFieldId( + r'ACTION_DREAMING_STOPPED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_DREAMING_STOPPED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_DREAMING_STOPPED => + _id_ACTION_DREAMING_STOPPED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_EDIT = _class.staticFieldId( + r'ACTION_EDIT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_EDIT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_EDIT => + _id_ACTION_EDIT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_EXTERNAL_APPLICATIONS_AVAILABLE = _class + .staticFieldId( + r'ACTION_EXTERNAL_APPLICATIONS_AVAILABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_EXTERNAL_APPLICATIONS_AVAILABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_EXTERNAL_APPLICATIONS_AVAILABLE => + _id_ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE = _class + .staticFieldId( + r'ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE => + _id_ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_FACTORY_TEST = _class.staticFieldId( + r'ACTION_FACTORY_TEST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_FACTORY_TEST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_FACTORY_TEST => + _id_ACTION_FACTORY_TEST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_GET_CONTENT = _class.staticFieldId( + r'ACTION_GET_CONTENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_GET_CONTENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_GET_CONTENT => + _id_ACTION_GET_CONTENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_GET_RESTRICTION_ENTRIES = _class.staticFieldId( + r'ACTION_GET_RESTRICTION_ENTRIES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_GET_RESTRICTION_ENTRIES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_GET_RESTRICTION_ENTRIES => + _id_ACTION_GET_RESTRICTION_ENTRIES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_GTALK_SERVICE_CONNECTED = _class.staticFieldId( + r'ACTION_GTALK_SERVICE_CONNECTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_GTALK_SERVICE_CONNECTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_GTALK_SERVICE_CONNECTED => + _id_ACTION_GTALK_SERVICE_CONNECTED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_GTALK_SERVICE_DISCONNECTED = _class.staticFieldId( + r'ACTION_GTALK_SERVICE_DISCONNECTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_GTALK_SERVICE_DISCONNECTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_GTALK_SERVICE_DISCONNECTED => + _id_ACTION_GTALK_SERVICE_DISCONNECTED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_HEADSET_PLUG = _class.staticFieldId( + r'ACTION_HEADSET_PLUG', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_HEADSET_PLUG` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_HEADSET_PLUG => + _id_ACTION_HEADSET_PLUG.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_INPUT_METHOD_CHANGED = _class.staticFieldId( + r'ACTION_INPUT_METHOD_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_INPUT_METHOD_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_INPUT_METHOD_CHANGED => + _id_ACTION_INPUT_METHOD_CHANGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_INSERT = _class.staticFieldId( + r'ACTION_INSERT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_INSERT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_INSERT => + _id_ACTION_INSERT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_INSERT_OR_EDIT = _class.staticFieldId( + r'ACTION_INSERT_OR_EDIT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_INSERT_OR_EDIT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_INSERT_OR_EDIT => _id_ACTION_INSERT_OR_EDIT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_INSTALL_FAILURE = _class.staticFieldId( + r'ACTION_INSTALL_FAILURE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_INSTALL_FAILURE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_INSTALL_FAILURE => _id_ACTION_INSTALL_FAILURE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_INSTALL_PACKAGE = _class.staticFieldId( + r'ACTION_INSTALL_PACKAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_INSTALL_PACKAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_INSTALL_PACKAGE => _id_ACTION_INSTALL_PACKAGE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE = _class + .staticFieldId( + r'ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE => + _id_ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_LOCALE_CHANGED = _class.staticFieldId( + r'ACTION_LOCALE_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_LOCALE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_LOCALE_CHANGED => _id_ACTION_LOCALE_CHANGED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_LOCKED_BOOT_COMPLETED = _class.staticFieldId( + r'ACTION_LOCKED_BOOT_COMPLETED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_LOCKED_BOOT_COMPLETED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_LOCKED_BOOT_COMPLETED => + _id_ACTION_LOCKED_BOOT_COMPLETED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MAIN = _class.staticFieldId( + r'ACTION_MAIN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MAIN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MAIN => + _id_ACTION_MAIN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MANAGED_PROFILE_ADDED = _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_ADDED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_ADDED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MANAGED_PROFILE_ADDED => + _id_ACTION_MANAGED_PROFILE_ADDED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MANAGED_PROFILE_AVAILABLE = _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_AVAILABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_AVAILABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MANAGED_PROFILE_AVAILABLE => + _id_ACTION_MANAGED_PROFILE_AVAILABLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MANAGED_PROFILE_REMOVED = _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_REMOVED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_REMOVED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MANAGED_PROFILE_REMOVED => + _id_ACTION_MANAGED_PROFILE_REMOVED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MANAGED_PROFILE_UNAVAILABLE = _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_UNAVAILABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_UNAVAILABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MANAGED_PROFILE_UNAVAILABLE => + _id_ACTION_MANAGED_PROFILE_UNAVAILABLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MANAGED_PROFILE_UNLOCKED = _class.staticFieldId( + r'ACTION_MANAGED_PROFILE_UNLOCKED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MANAGED_PROFILE_UNLOCKED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MANAGED_PROFILE_UNLOCKED => + _id_ACTION_MANAGED_PROFILE_UNLOCKED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MANAGE_NETWORK_USAGE = _class.staticFieldId( + r'ACTION_MANAGE_NETWORK_USAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MANAGE_NETWORK_USAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MANAGE_NETWORK_USAGE => + _id_ACTION_MANAGE_NETWORK_USAGE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MANAGE_PACKAGE_STORAGE = _class.staticFieldId( + r'ACTION_MANAGE_PACKAGE_STORAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MANAGE_PACKAGE_STORAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MANAGE_PACKAGE_STORAGE => + _id_ACTION_MANAGE_PACKAGE_STORAGE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MANAGE_UNUSED_APPS = _class.staticFieldId( + r'ACTION_MANAGE_UNUSED_APPS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MANAGE_UNUSED_APPS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MANAGE_UNUSED_APPS => + _id_ACTION_MANAGE_UNUSED_APPS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MEDIA_BAD_REMOVAL = _class.staticFieldId( + r'ACTION_MEDIA_BAD_REMOVAL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_BAD_REMOVAL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_BAD_REMOVAL => + _id_ACTION_MEDIA_BAD_REMOVAL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MEDIA_BUTTON = _class.staticFieldId( + r'ACTION_MEDIA_BUTTON', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_BUTTON` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_BUTTON => + _id_ACTION_MEDIA_BUTTON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_CHECKING = _class.staticFieldId( + r'ACTION_MEDIA_CHECKING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_CHECKING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_CHECKING => _id_ACTION_MEDIA_CHECKING + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_EJECT = _class.staticFieldId( + r'ACTION_MEDIA_EJECT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_EJECT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_EJECT => + _id_ACTION_MEDIA_EJECT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_MOUNTED = _class.staticFieldId( + r'ACTION_MEDIA_MOUNTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_MOUNTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_MOUNTED => _id_ACTION_MEDIA_MOUNTED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_NOFS = _class.staticFieldId( + r'ACTION_MEDIA_NOFS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_NOFS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_NOFS => + _id_ACTION_MEDIA_NOFS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_REMOVED = _class.staticFieldId( + r'ACTION_MEDIA_REMOVED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_REMOVED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_REMOVED => _id_ACTION_MEDIA_REMOVED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_SCANNER_FINISHED = _class.staticFieldId( + r'ACTION_MEDIA_SCANNER_FINISHED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_SCANNER_FINISHED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_SCANNER_FINISHED => + _id_ACTION_MEDIA_SCANNER_FINISHED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MEDIA_SCANNER_SCAN_FILE = _class.staticFieldId( + r'ACTION_MEDIA_SCANNER_SCAN_FILE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_SCANNER_SCAN_FILE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_SCANNER_SCAN_FILE => + _id_ACTION_MEDIA_SCANNER_SCAN_FILE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MEDIA_SCANNER_STARTED = _class.staticFieldId( + r'ACTION_MEDIA_SCANNER_STARTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_SCANNER_STARTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_SCANNER_STARTED => + _id_ACTION_MEDIA_SCANNER_STARTED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MEDIA_SHARED = _class.staticFieldId( + r'ACTION_MEDIA_SHARED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_SHARED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_SHARED => + _id_ACTION_MEDIA_SHARED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MEDIA_UNMOUNTABLE = _class.staticFieldId( + r'ACTION_MEDIA_UNMOUNTABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_UNMOUNTABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_UNMOUNTABLE => + _id_ACTION_MEDIA_UNMOUNTABLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MEDIA_UNMOUNTED = _class.staticFieldId( + r'ACTION_MEDIA_UNMOUNTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MEDIA_UNMOUNTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MEDIA_UNMOUNTED => _id_ACTION_MEDIA_UNMOUNTED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_MY_PACKAGE_REPLACED = _class.staticFieldId( + r'ACTION_MY_PACKAGE_REPLACED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MY_PACKAGE_REPLACED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MY_PACKAGE_REPLACED => + _id_ACTION_MY_PACKAGE_REPLACED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MY_PACKAGE_SUSPENDED = _class.staticFieldId( + r'ACTION_MY_PACKAGE_SUSPENDED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MY_PACKAGE_SUSPENDED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MY_PACKAGE_SUSPENDED => + _id_ACTION_MY_PACKAGE_SUSPENDED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_MY_PACKAGE_UNSUSPENDED = _class.staticFieldId( + r'ACTION_MY_PACKAGE_UNSUSPENDED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_MY_PACKAGE_UNSUSPENDED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_MY_PACKAGE_UNSUSPENDED => + _id_ACTION_MY_PACKAGE_UNSUSPENDED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_NEW_OUTGOING_CALL = _class.staticFieldId( + r'ACTION_NEW_OUTGOING_CALL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_NEW_OUTGOING_CALL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_NEW_OUTGOING_CALL => + _id_ACTION_NEW_OUTGOING_CALL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_OPEN_DOCUMENT = _class.staticFieldId( + r'ACTION_OPEN_DOCUMENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_OPEN_DOCUMENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_OPEN_DOCUMENT => _id_ACTION_OPEN_DOCUMENT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_OPEN_DOCUMENT_TREE = _class.staticFieldId( + r'ACTION_OPEN_DOCUMENT_TREE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_OPEN_DOCUMENT_TREE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_OPEN_DOCUMENT_TREE => + _id_ACTION_OPEN_DOCUMENT_TREE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGES_SUSPENDED = _class.staticFieldId( + r'ACTION_PACKAGES_SUSPENDED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGES_SUSPENDED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGES_SUSPENDED => + _id_ACTION_PACKAGES_SUSPENDED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGES_UNSUSPENDED = _class.staticFieldId( + r'ACTION_PACKAGES_UNSUSPENDED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGES_UNSUSPENDED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGES_UNSUSPENDED => + _id_ACTION_PACKAGES_UNSUSPENDED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGE_ADDED = _class.staticFieldId( + r'ACTION_PACKAGE_ADDED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_ADDED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_ADDED => _id_ACTION_PACKAGE_ADDED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_CHANGED = _class.staticFieldId( + r'ACTION_PACKAGE_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_CHANGED => _id_ACTION_PACKAGE_CHANGED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_DATA_CLEARED = _class.staticFieldId( + r'ACTION_PACKAGE_DATA_CLEARED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_DATA_CLEARED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_DATA_CLEARED => + _id_ACTION_PACKAGE_DATA_CLEARED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGE_FIRST_LAUNCH = _class.staticFieldId( + r'ACTION_PACKAGE_FIRST_LAUNCH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_FIRST_LAUNCH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_FIRST_LAUNCH => + _id_ACTION_PACKAGE_FIRST_LAUNCH.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGE_FULLY_REMOVED = _class.staticFieldId( + r'ACTION_PACKAGE_FULLY_REMOVED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_FULLY_REMOVED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_FULLY_REMOVED => + _id_ACTION_PACKAGE_FULLY_REMOVED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGE_INSTALL = _class.staticFieldId( + r'ACTION_PACKAGE_INSTALL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_INSTALL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_INSTALL => _id_ACTION_PACKAGE_INSTALL + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_NEEDS_VERIFICATION = _class.staticFieldId( + r'ACTION_PACKAGE_NEEDS_VERIFICATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_NEEDS_VERIFICATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_NEEDS_VERIFICATION => + _id_ACTION_PACKAGE_NEEDS_VERIFICATION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGE_REMOVED = _class.staticFieldId( + r'ACTION_PACKAGE_REMOVED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_REMOVED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_REMOVED => _id_ACTION_PACKAGE_REMOVED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PACKAGE_REPLACED = _class.staticFieldId( + r'ACTION_PACKAGE_REPLACED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_REPLACED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_REPLACED => + _id_ACTION_PACKAGE_REPLACED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGE_RESTARTED = _class.staticFieldId( + r'ACTION_PACKAGE_RESTARTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_RESTARTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_RESTARTED => + _id_ACTION_PACKAGE_RESTARTED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGE_UNSTOPPED = _class.staticFieldId( + r'ACTION_PACKAGE_UNSTOPPED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_UNSTOPPED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_UNSTOPPED => + _id_ACTION_PACKAGE_UNSTOPPED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PACKAGE_VERIFIED = _class.staticFieldId( + r'ACTION_PACKAGE_VERIFIED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PACKAGE_VERIFIED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PACKAGE_VERIFIED => + _id_ACTION_PACKAGE_VERIFIED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PASTE = _class.staticFieldId( + r'ACTION_PASTE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PASTE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PASTE => + _id_ACTION_PASTE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PICK = _class.staticFieldId( + r'ACTION_PICK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PICK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PICK => + _id_ACTION_PICK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PICK_ACTIVITY = _class.staticFieldId( + r'ACTION_PICK_ACTIVITY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PICK_ACTIVITY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PICK_ACTIVITY => _id_ACTION_PICK_ACTIVITY + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_POWER_CONNECTED = _class.staticFieldId( + r'ACTION_POWER_CONNECTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_POWER_CONNECTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_POWER_CONNECTED => _id_ACTION_POWER_CONNECTED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_POWER_DISCONNECTED = _class.staticFieldId( + r'ACTION_POWER_DISCONNECTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_POWER_DISCONNECTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_POWER_DISCONNECTED => + _id_ACTION_POWER_DISCONNECTED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_POWER_USAGE_SUMMARY = _class.staticFieldId( + r'ACTION_POWER_USAGE_SUMMARY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_POWER_USAGE_SUMMARY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_POWER_USAGE_SUMMARY => + _id_ACTION_POWER_USAGE_SUMMARY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PROCESS_TEXT = _class.staticFieldId( + r'ACTION_PROCESS_TEXT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PROCESS_TEXT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PROCESS_TEXT => + _id_ACTION_PROCESS_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROFILE_ACCESSIBLE = _class.staticFieldId( + r'ACTION_PROFILE_ACCESSIBLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PROFILE_ACCESSIBLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PROFILE_ACCESSIBLE => + _id_ACTION_PROFILE_ACCESSIBLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PROFILE_ADDED = _class.staticFieldId( + r'ACTION_PROFILE_ADDED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PROFILE_ADDED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PROFILE_ADDED => _id_ACTION_PROFILE_ADDED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROFILE_AVAILABLE = _class.staticFieldId( + r'ACTION_PROFILE_AVAILABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PROFILE_AVAILABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PROFILE_AVAILABLE => + _id_ACTION_PROFILE_AVAILABLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PROFILE_INACCESSIBLE = _class.staticFieldId( + r'ACTION_PROFILE_INACCESSIBLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PROFILE_INACCESSIBLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PROFILE_INACCESSIBLE => + _id_ACTION_PROFILE_INACCESSIBLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PROFILE_REMOVED = _class.staticFieldId( + r'ACTION_PROFILE_REMOVED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PROFILE_REMOVED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PROFILE_REMOVED => _id_ACTION_PROFILE_REMOVED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_PROFILE_UNAVAILABLE = _class.staticFieldId( + r'ACTION_PROFILE_UNAVAILABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PROFILE_UNAVAILABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PROFILE_UNAVAILABLE => + _id_ACTION_PROFILE_UNAVAILABLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_PROVIDER_CHANGED = _class.staticFieldId( + r'ACTION_PROVIDER_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_PROVIDER_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_PROVIDER_CHANGED => + _id_ACTION_PROVIDER_CHANGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_QUICK_CLOCK = _class.staticFieldId( + r'ACTION_QUICK_CLOCK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_QUICK_CLOCK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_QUICK_CLOCK => + _id_ACTION_QUICK_CLOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_QUICK_VIEW = _class.staticFieldId( + r'ACTION_QUICK_VIEW', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_QUICK_VIEW` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_QUICK_VIEW => + _id_ACTION_QUICK_VIEW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_REBOOT = _class.staticFieldId( + r'ACTION_REBOOT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_REBOOT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_REBOOT => + _id_ACTION_REBOOT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_RUN = _class.staticFieldId( + r'ACTION_RUN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_RUN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_RUN => + _id_ACTION_RUN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SAFETY_CENTER = _class.staticFieldId( + r'ACTION_SAFETY_CENTER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SAFETY_CENTER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SAFETY_CENTER => _id_ACTION_SAFETY_CENTER + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SCREEN_OFF = _class.staticFieldId( + r'ACTION_SCREEN_OFF', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SCREEN_OFF` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SCREEN_OFF => + _id_ACTION_SCREEN_OFF.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SCREEN_ON = _class.staticFieldId( + r'ACTION_SCREEN_ON', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SCREEN_ON` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SCREEN_ON => + _id_ACTION_SCREEN_ON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SEARCH = _class.staticFieldId( + r'ACTION_SEARCH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SEARCH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SEARCH => + _id_ACTION_SEARCH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SEARCH_LONG_PRESS = _class.staticFieldId( + r'ACTION_SEARCH_LONG_PRESS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SEARCH_LONG_PRESS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SEARCH_LONG_PRESS => + _id_ACTION_SEARCH_LONG_PRESS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_SEND = _class.staticFieldId( + r'ACTION_SEND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SEND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SEND => + _id_ACTION_SEND.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SENDTO = _class.staticFieldId( + r'ACTION_SENDTO', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SENDTO` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SENDTO => + _id_ACTION_SENDTO.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SEND_MULTIPLE = _class.staticFieldId( + r'ACTION_SEND_MULTIPLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SEND_MULTIPLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SEND_MULTIPLE => _id_ACTION_SEND_MULTIPLE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SET_WALLPAPER = _class.staticFieldId( + r'ACTION_SET_WALLPAPER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SET_WALLPAPER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SET_WALLPAPER => _id_ACTION_SET_WALLPAPER + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SHOW_APP_INFO = _class.staticFieldId( + r'ACTION_SHOW_APP_INFO', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SHOW_APP_INFO` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SHOW_APP_INFO => _id_ACTION_SHOW_APP_INFO + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SHOW_WORK_APPS = _class.staticFieldId( + r'ACTION_SHOW_WORK_APPS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SHOW_WORK_APPS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SHOW_WORK_APPS => _id_ACTION_SHOW_WORK_APPS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SHUTDOWN = _class.staticFieldId( + r'ACTION_SHUTDOWN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SHUTDOWN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SHUTDOWN => + _id_ACTION_SHUTDOWN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SYNC = _class.staticFieldId( + r'ACTION_SYNC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SYNC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SYNC => + _id_ACTION_SYNC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_SYSTEM_TUTORIAL = _class.staticFieldId( + r'ACTION_SYSTEM_TUTORIAL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_SYSTEM_TUTORIAL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_SYSTEM_TUTORIAL => _id_ACTION_SYSTEM_TUTORIAL + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_TIMEZONE_CHANGED = _class.staticFieldId( + r'ACTION_TIMEZONE_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_TIMEZONE_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_TIMEZONE_CHANGED => + _id_ACTION_TIMEZONE_CHANGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_TIME_CHANGED = _class.staticFieldId( + r'ACTION_TIME_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_TIME_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_TIME_CHANGED => + _id_ACTION_TIME_CHANGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_TIME_TICK = _class.staticFieldId( + r'ACTION_TIME_TICK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_TIME_TICK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_TIME_TICK => + _id_ACTION_TIME_TICK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_TRANSLATE = _class.staticFieldId( + r'ACTION_TRANSLATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_TRANSLATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_TRANSLATE => + _id_ACTION_TRANSLATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_UID_REMOVED = _class.staticFieldId( + r'ACTION_UID_REMOVED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_UID_REMOVED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_UID_REMOVED => + _id_ACTION_UID_REMOVED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_UMS_CONNECTED = _class.staticFieldId( + r'ACTION_UMS_CONNECTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_UMS_CONNECTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_UMS_CONNECTED => _id_ACTION_UMS_CONNECTED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_UMS_DISCONNECTED = _class.staticFieldId( + r'ACTION_UMS_DISCONNECTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_UMS_DISCONNECTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_UMS_DISCONNECTED => + _id_ACTION_UMS_DISCONNECTED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_UNARCHIVE_PACKAGE = _class.staticFieldId( + r'ACTION_UNARCHIVE_PACKAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_UNARCHIVE_PACKAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_UNARCHIVE_PACKAGE => + _id_ACTION_UNARCHIVE_PACKAGE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_UNINSTALL_PACKAGE = _class.staticFieldId( + r'ACTION_UNINSTALL_PACKAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_UNINSTALL_PACKAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_UNINSTALL_PACKAGE => + _id_ACTION_UNINSTALL_PACKAGE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_USER_BACKGROUND = _class.staticFieldId( + r'ACTION_USER_BACKGROUND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_USER_BACKGROUND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_USER_BACKGROUND => _id_ACTION_USER_BACKGROUND + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_USER_FOREGROUND = _class.staticFieldId( + r'ACTION_USER_FOREGROUND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_USER_FOREGROUND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_USER_FOREGROUND => _id_ACTION_USER_FOREGROUND + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_USER_INITIALIZE = _class.staticFieldId( + r'ACTION_USER_INITIALIZE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_USER_INITIALIZE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_USER_INITIALIZE => _id_ACTION_USER_INITIALIZE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_USER_PRESENT = _class.staticFieldId( + r'ACTION_USER_PRESENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_USER_PRESENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_USER_PRESENT => + _id_ACTION_USER_PRESENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_USER_UNLOCKED = _class.staticFieldId( + r'ACTION_USER_UNLOCKED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_USER_UNLOCKED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_USER_UNLOCKED => _id_ACTION_USER_UNLOCKED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_VIEW = _class.staticFieldId( + r'ACTION_VIEW', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_VIEW` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_VIEW => + _id_ACTION_VIEW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_VIEW_LOCUS = _class.staticFieldId( + r'ACTION_VIEW_LOCUS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_VIEW_LOCUS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_VIEW_LOCUS => + _id_ACTION_VIEW_LOCUS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_VIEW_PERMISSION_USAGE = _class.staticFieldId( + r'ACTION_VIEW_PERMISSION_USAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_VIEW_PERMISSION_USAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_VIEW_PERMISSION_USAGE => + _id_ACTION_VIEW_PERMISSION_USAGE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD = _class + .staticFieldId( + r'ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD => + _id_ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_VOICE_COMMAND = _class.staticFieldId( + r'ACTION_VOICE_COMMAND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_VOICE_COMMAND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_VOICE_COMMAND => _id_ACTION_VOICE_COMMAND + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTION_WALLPAPER_CHANGED = _class.staticFieldId( + r'ACTION_WALLPAPER_CHANGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_WALLPAPER_CHANGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_WALLPAPER_CHANGED => + _id_ACTION_WALLPAPER_CHANGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACTION_WEB_SEARCH = _class.staticFieldId( + r'ACTION_WEB_SEARCH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTION_WEB_SEARCH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTION_WEB_SEARCH => + _id_ACTION_WEB_SEARCH.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN` + static const CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN = 4; + + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_FAILED` + static const CAPTURE_CONTENT_FOR_NOTE_FAILED = 1; + + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_SUCCESS` + static const CAPTURE_CONTENT_FOR_NOTE_SUCCESS = 0; + + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED` + static const CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED = 2; + + /// from: `static public final int CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED` + static const CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED = 3; + static final _id_CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET = _class + .staticFieldId( + r'CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET => + _id_CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CATEGORY_ALTERNATIVE = _class.staticFieldId( + r'CATEGORY_ALTERNATIVE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_ALTERNATIVE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_ALTERNATIVE => _id_CATEGORY_ALTERNATIVE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_BROWSER = _class.staticFieldId( + r'CATEGORY_APP_BROWSER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_BROWSER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_BROWSER => _id_CATEGORY_APP_BROWSER + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_CALCULATOR = _class.staticFieldId( + r'CATEGORY_APP_CALCULATOR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_CALCULATOR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_CALCULATOR => + _id_CATEGORY_APP_CALCULATOR.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CATEGORY_APP_CALENDAR = _class.staticFieldId( + r'CATEGORY_APP_CALENDAR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_CALENDAR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_CALENDAR => _id_CATEGORY_APP_CALENDAR + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_CONTACTS = _class.staticFieldId( + r'CATEGORY_APP_CONTACTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_CONTACTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_CONTACTS => _id_CATEGORY_APP_CONTACTS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_EMAIL = _class.staticFieldId( + r'CATEGORY_APP_EMAIL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_EMAIL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_EMAIL => + _id_CATEGORY_APP_EMAIL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_FILES = _class.staticFieldId( + r'CATEGORY_APP_FILES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_FILES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_FILES => + _id_CATEGORY_APP_FILES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_FITNESS = _class.staticFieldId( + r'CATEGORY_APP_FITNESS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_FITNESS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_FITNESS => _id_CATEGORY_APP_FITNESS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_GALLERY = _class.staticFieldId( + r'CATEGORY_APP_GALLERY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_GALLERY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_GALLERY => _id_CATEGORY_APP_GALLERY + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_MAPS = _class.staticFieldId( + r'CATEGORY_APP_MAPS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_MAPS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_MAPS => + _id_CATEGORY_APP_MAPS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_MARKET = _class.staticFieldId( + r'CATEGORY_APP_MARKET', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_MARKET` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_MARKET => + _id_CATEGORY_APP_MARKET.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_MESSAGING = _class.staticFieldId( + r'CATEGORY_APP_MESSAGING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_MESSAGING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_MESSAGING => _id_CATEGORY_APP_MESSAGING + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_MUSIC = _class.staticFieldId( + r'CATEGORY_APP_MUSIC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_MUSIC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_MUSIC => + _id_CATEGORY_APP_MUSIC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_APP_WEATHER = _class.staticFieldId( + r'CATEGORY_APP_WEATHER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_APP_WEATHER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_APP_WEATHER => _id_CATEGORY_APP_WEATHER + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_BROWSABLE = _class.staticFieldId( + r'CATEGORY_BROWSABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_BROWSABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_BROWSABLE => + _id_CATEGORY_BROWSABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_CAR_DOCK = _class.staticFieldId( + r'CATEGORY_CAR_DOCK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_CAR_DOCK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_CAR_DOCK => + _id_CATEGORY_CAR_DOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_CAR_MODE = _class.staticFieldId( + r'CATEGORY_CAR_MODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_CAR_MODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_CAR_MODE => + _id_CATEGORY_CAR_MODE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_DEFAULT = _class.staticFieldId( + r'CATEGORY_DEFAULT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_DEFAULT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_DEFAULT => + _id_CATEGORY_DEFAULT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_DESK_DOCK = _class.staticFieldId( + r'CATEGORY_DESK_DOCK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_DESK_DOCK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_DESK_DOCK => + _id_CATEGORY_DESK_DOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_DEVELOPMENT_PREFERENCE = _class.staticFieldId( + r'CATEGORY_DEVELOPMENT_PREFERENCE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_DEVELOPMENT_PREFERENCE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_DEVELOPMENT_PREFERENCE => + _id_CATEGORY_DEVELOPMENT_PREFERENCE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CATEGORY_EMBED = _class.staticFieldId( + r'CATEGORY_EMBED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_EMBED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_EMBED => + _id_CATEGORY_EMBED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST = _class + .staticFieldId( + r'CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST => + _id_CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CATEGORY_HE_DESK_DOCK = _class.staticFieldId( + r'CATEGORY_HE_DESK_DOCK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_HE_DESK_DOCK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_HE_DESK_DOCK => _id_CATEGORY_HE_DESK_DOCK + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_HOME = _class.staticFieldId( + r'CATEGORY_HOME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_HOME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_HOME => + _id_CATEGORY_HOME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_INFO = _class.staticFieldId( + r'CATEGORY_INFO', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_INFO` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_INFO => + _id_CATEGORY_INFO.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_LAUNCHER = _class.staticFieldId( + r'CATEGORY_LAUNCHER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_LAUNCHER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_LAUNCHER => + _id_CATEGORY_LAUNCHER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_LEANBACK_LAUNCHER = _class.staticFieldId( + r'CATEGORY_LEANBACK_LAUNCHER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_LEANBACK_LAUNCHER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_LEANBACK_LAUNCHER => + _id_CATEGORY_LEANBACK_LAUNCHER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CATEGORY_LE_DESK_DOCK = _class.staticFieldId( + r'CATEGORY_LE_DESK_DOCK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_LE_DESK_DOCK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_LE_DESK_DOCK => _id_CATEGORY_LE_DESK_DOCK + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_MONKEY = _class.staticFieldId( + r'CATEGORY_MONKEY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_MONKEY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_MONKEY => + _id_CATEGORY_MONKEY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_OPENABLE = _class.staticFieldId( + r'CATEGORY_OPENABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_OPENABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_OPENABLE => + _id_CATEGORY_OPENABLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_PREFERENCE = _class.staticFieldId( + r'CATEGORY_PREFERENCE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_PREFERENCE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_PREFERENCE => + _id_CATEGORY_PREFERENCE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_SAMPLE_CODE = _class.staticFieldId( + r'CATEGORY_SAMPLE_CODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_SAMPLE_CODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_SAMPLE_CODE => _id_CATEGORY_SAMPLE_CODE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_SECONDARY_HOME = _class.staticFieldId( + r'CATEGORY_SECONDARY_HOME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_SECONDARY_HOME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_SECONDARY_HOME => + _id_CATEGORY_SECONDARY_HOME.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CATEGORY_SELECTED_ALTERNATIVE = _class.staticFieldId( + r'CATEGORY_SELECTED_ALTERNATIVE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_SELECTED_ALTERNATIVE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_SELECTED_ALTERNATIVE => + _id_CATEGORY_SELECTED_ALTERNATIVE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CATEGORY_TAB = _class.staticFieldId( + r'CATEGORY_TAB', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_TAB` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_TAB => + _id_CATEGORY_TAB.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_TEST = _class.staticFieldId( + r'CATEGORY_TEST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_TEST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_TEST => + _id_CATEGORY_TEST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_TYPED_OPENABLE = _class.staticFieldId( + r'CATEGORY_TYPED_OPENABLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_TYPED_OPENABLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_TYPED_OPENABLE => + _id_CATEGORY_TYPED_OPENABLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CATEGORY_UNIT_TEST = _class.staticFieldId( + r'CATEGORY_UNIT_TEST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_UNIT_TEST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_UNIT_TEST => + _id_CATEGORY_UNIT_TEST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_VOICE = _class.staticFieldId( + r'CATEGORY_VOICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_VOICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_VOICE => + _id_CATEGORY_VOICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CATEGORY_VR_HOME = _class.staticFieldId( + r'CATEGORY_VR_HOME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CATEGORY_VR_HOME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CATEGORY_VR_HOME => + _id_CATEGORY_VR_HOME.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int CHOOSER_CONTENT_TYPE_ALBUM` + static const CHOOSER_CONTENT_TYPE_ALBUM = 1; + static final _id_CREATOR = _class.staticFieldId( + r'CREATOR', + r'Landroid/os/Parcelable$Creator;', + ); + + /// from: `static public final android.os.Parcelable$Creator CREATOR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? get CREATOR => + _id_CREATOR.get(_class, const jni$_.$JObject$NullableType$()); + + static final _id_EXTRA_ALARM_COUNT = _class.staticFieldId( + r'EXTRA_ALARM_COUNT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ALARM_COUNT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ALARM_COUNT => + _id_EXTRA_ALARM_COUNT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ALLOW_MULTIPLE = _class.staticFieldId( + r'EXTRA_ALLOW_MULTIPLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ALLOW_MULTIPLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ALLOW_MULTIPLE => _id_EXTRA_ALLOW_MULTIPLE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ALLOW_REPLACE = _class.staticFieldId( + r'EXTRA_ALLOW_REPLACE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ALLOW_REPLACE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ALLOW_REPLACE => + _id_EXTRA_ALLOW_REPLACE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ALTERNATE_INTENTS = _class.staticFieldId( + r'EXTRA_ALTERNATE_INTENTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ALTERNATE_INTENTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ALTERNATE_INTENTS => + _id_EXTRA_ALTERNATE_INTENTS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_ARCHIVAL = _class.staticFieldId( + r'EXTRA_ARCHIVAL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ARCHIVAL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ARCHIVAL => + _id_EXTRA_ARCHIVAL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ASSIST_CONTEXT = _class.staticFieldId( + r'EXTRA_ASSIST_CONTEXT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ASSIST_CONTEXT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ASSIST_CONTEXT => _id_EXTRA_ASSIST_CONTEXT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ASSIST_INPUT_DEVICE_ID = _class.staticFieldId( + r'EXTRA_ASSIST_INPUT_DEVICE_ID', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ASSIST_INPUT_DEVICE_ID` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ASSIST_INPUT_DEVICE_ID => + _id_EXTRA_ASSIST_INPUT_DEVICE_ID.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_ASSIST_INPUT_HINT_KEYBOARD = _class.staticFieldId( + r'EXTRA_ASSIST_INPUT_HINT_KEYBOARD', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ASSIST_INPUT_HINT_KEYBOARD` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ASSIST_INPUT_HINT_KEYBOARD => + _id_EXTRA_ASSIST_INPUT_HINT_KEYBOARD.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_ASSIST_PACKAGE = _class.staticFieldId( + r'EXTRA_ASSIST_PACKAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ASSIST_PACKAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ASSIST_PACKAGE => _id_EXTRA_ASSIST_PACKAGE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ASSIST_UID = _class.staticFieldId( + r'EXTRA_ASSIST_UID', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ASSIST_UID` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ASSIST_UID => + _id_EXTRA_ASSIST_UID.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_ATTRIBUTION_TAGS = _class.staticFieldId( + r'EXTRA_ATTRIBUTION_TAGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ATTRIBUTION_TAGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ATTRIBUTION_TAGS => _id_EXTRA_ATTRIBUTION_TAGS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_AUTO_LAUNCH_SINGLE_CHOICE = _class.staticFieldId( + r'EXTRA_AUTO_LAUNCH_SINGLE_CHOICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_AUTO_LAUNCH_SINGLE_CHOICE => + _id_EXTRA_AUTO_LAUNCH_SINGLE_CHOICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_BCC = _class.staticFieldId( + r'EXTRA_BCC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_BCC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_BCC => + _id_EXTRA_BCC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_BUG_REPORT = _class.staticFieldId( + r'EXTRA_BUG_REPORT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_BUG_REPORT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_BUG_REPORT => + _id_EXTRA_BUG_REPORT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE = _class + .staticFieldId( + r'EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE => + _id_EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CC = _class.staticFieldId( + r'EXTRA_CC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CC => + _id_EXTRA_CC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHANGED_COMPONENT_NAME = _class.staticFieldId( + r'EXTRA_CHANGED_COMPONENT_NAME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHANGED_COMPONENT_NAME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHANGED_COMPONENT_NAME => + _id_EXTRA_CHANGED_COMPONENT_NAME.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHANGED_COMPONENT_NAME_LIST = _class.staticFieldId( + r'EXTRA_CHANGED_COMPONENT_NAME_LIST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHANGED_COMPONENT_NAME_LIST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHANGED_COMPONENT_NAME_LIST => + _id_EXTRA_CHANGED_COMPONENT_NAME_LIST.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHANGED_PACKAGE_LIST = _class.staticFieldId( + r'EXTRA_CHANGED_PACKAGE_LIST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHANGED_PACKAGE_LIST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHANGED_PACKAGE_LIST => + _id_EXTRA_CHANGED_PACKAGE_LIST.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHANGED_UID_LIST = _class.staticFieldId( + r'EXTRA_CHANGED_UID_LIST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHANGED_UID_LIST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHANGED_UID_LIST => _id_EXTRA_CHANGED_UID_LIST + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI = _class.staticFieldId( + r'EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI => + _id_EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHOOSER_CONTENT_TYPE_HINT = _class.staticFieldId( + r'EXTRA_CHOOSER_CONTENT_TYPE_HINT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOOSER_CONTENT_TYPE_HINT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOOSER_CONTENT_TYPE_HINT => + _id_EXTRA_CHOOSER_CONTENT_TYPE_HINT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHOOSER_CUSTOM_ACTIONS = _class.staticFieldId( + r'EXTRA_CHOOSER_CUSTOM_ACTIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOOSER_CUSTOM_ACTIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOOSER_CUSTOM_ACTIONS => + _id_EXTRA_CHOOSER_CUSTOM_ACTIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHOOSER_FOCUSED_ITEM_POSITION = _class.staticFieldId( + r'EXTRA_CHOOSER_FOCUSED_ITEM_POSITION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOOSER_FOCUSED_ITEM_POSITION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOOSER_FOCUSED_ITEM_POSITION => + _id_EXTRA_CHOOSER_FOCUSED_ITEM_POSITION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHOOSER_MODIFY_SHARE_ACTION = _class.staticFieldId( + r'EXTRA_CHOOSER_MODIFY_SHARE_ACTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOOSER_MODIFY_SHARE_ACTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOOSER_MODIFY_SHARE_ACTION => + _id_EXTRA_CHOOSER_MODIFY_SHARE_ACTION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = _class + .staticFieldId( + r'EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER => + _id_EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHOOSER_RESULT = _class.staticFieldId( + r'EXTRA_CHOOSER_RESULT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOOSER_RESULT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOOSER_RESULT => _id_EXTRA_CHOOSER_RESULT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOOSER_RESULT_INTENT_SENDER = _class.staticFieldId( + r'EXTRA_CHOOSER_RESULT_INTENT_SENDER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOOSER_RESULT_INTENT_SENDER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOOSER_RESULT_INTENT_SENDER => + _id_EXTRA_CHOOSER_RESULT_INTENT_SENDER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CHOOSER_TARGETS = _class.staticFieldId( + r'EXTRA_CHOOSER_TARGETS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOOSER_TARGETS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOOSER_TARGETS => _id_EXTRA_CHOOSER_TARGETS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOSEN_COMPONENT = _class.staticFieldId( + r'EXTRA_CHOSEN_COMPONENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOSEN_COMPONENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOSEN_COMPONENT => _id_EXTRA_CHOSEN_COMPONENT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = _class.staticFieldId( + r'EXTRA_CHOSEN_COMPONENT_INTENT_SENDER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CHOSEN_COMPONENT_INTENT_SENDER => + _id_EXTRA_CHOSEN_COMPONENT_INTENT_SENDER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_COMPONENT_NAME = _class.staticFieldId( + r'EXTRA_COMPONENT_NAME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_COMPONENT_NAME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_COMPONENT_NAME => _id_EXTRA_COMPONENT_NAME + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_CONTENT_ANNOTATIONS = _class.staticFieldId( + r'EXTRA_CONTENT_ANNOTATIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CONTENT_ANNOTATIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CONTENT_ANNOTATIONS => + _id_EXTRA_CONTENT_ANNOTATIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_CONTENT_QUERY = _class.staticFieldId( + r'EXTRA_CONTENT_QUERY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_CONTENT_QUERY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_CONTENT_QUERY => + _id_EXTRA_CONTENT_QUERY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_DATA_REMOVED = _class.staticFieldId( + r'EXTRA_DATA_REMOVED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_DATA_REMOVED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_DATA_REMOVED => + _id_EXTRA_DATA_REMOVED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_DOCK_STATE = _class.staticFieldId( + r'EXTRA_DOCK_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_DOCK_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_DOCK_STATE => + _id_EXTRA_DOCK_STATE.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int EXTRA_DOCK_STATE_CAR` + static const EXTRA_DOCK_STATE_CAR = 2; + + /// from: `static public final int EXTRA_DOCK_STATE_DESK` + static const EXTRA_DOCK_STATE_DESK = 1; + + /// from: `static public final int EXTRA_DOCK_STATE_HE_DESK` + static const EXTRA_DOCK_STATE_HE_DESK = 4; + + /// from: `static public final int EXTRA_DOCK_STATE_LE_DESK` + static const EXTRA_DOCK_STATE_LE_DESK = 3; + + /// from: `static public final int EXTRA_DOCK_STATE_UNDOCKED` + static const EXTRA_DOCK_STATE_UNDOCKED = 0; + static final _id_EXTRA_DONT_KILL_APP = _class.staticFieldId( + r'EXTRA_DONT_KILL_APP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_DONT_KILL_APP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_DONT_KILL_APP => + _id_EXTRA_DONT_KILL_APP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_DURATION_MILLIS = _class.staticFieldId( + r'EXTRA_DURATION_MILLIS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_DURATION_MILLIS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_DURATION_MILLIS => _id_EXTRA_DURATION_MILLIS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_EMAIL = _class.staticFieldId( + r'EXTRA_EMAIL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_EMAIL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_EMAIL => + _id_EXTRA_EMAIL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_END_TIME = _class.staticFieldId( + r'EXTRA_END_TIME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_END_TIME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_END_TIME => + _id_EXTRA_END_TIME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_EXCLUDE_COMPONENTS = _class.staticFieldId( + r'EXTRA_EXCLUDE_COMPONENTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_EXCLUDE_COMPONENTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_EXCLUDE_COMPONENTS => + _id_EXTRA_EXCLUDE_COMPONENTS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_FROM_STORAGE = _class.staticFieldId( + r'EXTRA_FROM_STORAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_FROM_STORAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_FROM_STORAGE => + _id_EXTRA_FROM_STORAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_HTML_TEXT = _class.staticFieldId( + r'EXTRA_HTML_TEXT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_HTML_TEXT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_HTML_TEXT => + _id_EXTRA_HTML_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_INDEX = _class.staticFieldId( + r'EXTRA_INDEX', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_INDEX` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_INDEX => + _id_EXTRA_INDEX.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_INITIAL_INTENTS = _class.staticFieldId( + r'EXTRA_INITIAL_INTENTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_INITIAL_INTENTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_INITIAL_INTENTS => _id_EXTRA_INITIAL_INTENTS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_INSTALLER_PACKAGE_NAME = _class.staticFieldId( + r'EXTRA_INSTALLER_PACKAGE_NAME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_INSTALLER_PACKAGE_NAME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_INSTALLER_PACKAGE_NAME => + _id_EXTRA_INSTALLER_PACKAGE_NAME.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_INTENT = _class.staticFieldId( + r'EXTRA_INTENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_INTENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_INTENT => + _id_EXTRA_INTENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_KEY_EVENT = _class.staticFieldId( + r'EXTRA_KEY_EVENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_KEY_EVENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_KEY_EVENT => + _id_EXTRA_KEY_EVENT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_LOCALE_LIST = _class.staticFieldId( + r'EXTRA_LOCALE_LIST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_LOCALE_LIST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_LOCALE_LIST => + _id_EXTRA_LOCALE_LIST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_LOCAL_ONLY = _class.staticFieldId( + r'EXTRA_LOCAL_ONLY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_LOCAL_ONLY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_LOCAL_ONLY => + _id_EXTRA_LOCAL_ONLY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_LOCUS_ID = _class.staticFieldId( + r'EXTRA_LOCUS_ID', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_LOCUS_ID` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_LOCUS_ID => + _id_EXTRA_LOCUS_ID.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_METADATA_TEXT = _class.staticFieldId( + r'EXTRA_METADATA_TEXT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_METADATA_TEXT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_METADATA_TEXT => + _id_EXTRA_METADATA_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_MIME_TYPES = _class.staticFieldId( + r'EXTRA_MIME_TYPES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_MIME_TYPES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_MIME_TYPES => + _id_EXTRA_MIME_TYPES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_NOT_UNKNOWN_SOURCE = _class.staticFieldId( + r'EXTRA_NOT_UNKNOWN_SOURCE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_NOT_UNKNOWN_SOURCE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_NOT_UNKNOWN_SOURCE => + _id_EXTRA_NOT_UNKNOWN_SOURCE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_ORIGINATING_URI = _class.staticFieldId( + r'EXTRA_ORIGINATING_URI', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_ORIGINATING_URI` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_ORIGINATING_URI => _id_EXTRA_ORIGINATING_URI + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PACKAGES = _class.staticFieldId( + r'EXTRA_PACKAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_PACKAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_PACKAGES => + _id_EXTRA_PACKAGES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PACKAGE_NAME = _class.staticFieldId( + r'EXTRA_PACKAGE_NAME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_PACKAGE_NAME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_PACKAGE_NAME => + _id_EXTRA_PACKAGE_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PERMISSION_GROUP_NAME = _class.staticFieldId( + r'EXTRA_PERMISSION_GROUP_NAME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_PERMISSION_GROUP_NAME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_PERMISSION_GROUP_NAME => + _id_EXTRA_PERMISSION_GROUP_NAME.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_PHONE_NUMBER = _class.staticFieldId( + r'EXTRA_PHONE_NUMBER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_PHONE_NUMBER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_PHONE_NUMBER => + _id_EXTRA_PHONE_NUMBER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PROCESS_TEXT = _class.staticFieldId( + r'EXTRA_PROCESS_TEXT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_PROCESS_TEXT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_PROCESS_TEXT => + _id_EXTRA_PROCESS_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_PROCESS_TEXT_READONLY = _class.staticFieldId( + r'EXTRA_PROCESS_TEXT_READONLY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_PROCESS_TEXT_READONLY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_PROCESS_TEXT_READONLY => + _id_EXTRA_PROCESS_TEXT_READONLY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_QUICK_VIEW_FEATURES = _class.staticFieldId( + r'EXTRA_QUICK_VIEW_FEATURES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_QUICK_VIEW_FEATURES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_QUICK_VIEW_FEATURES => + _id_EXTRA_QUICK_VIEW_FEATURES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_QUIET_MODE = _class.staticFieldId( + r'EXTRA_QUIET_MODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_QUIET_MODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_QUIET_MODE => + _id_EXTRA_QUIET_MODE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_REFERRER = _class.staticFieldId( + r'EXTRA_REFERRER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_REFERRER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_REFERRER => + _id_EXTRA_REFERRER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_REFERRER_NAME = _class.staticFieldId( + r'EXTRA_REFERRER_NAME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_REFERRER_NAME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_REFERRER_NAME => + _id_EXTRA_REFERRER_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_REMOTE_INTENT_TOKEN = _class.staticFieldId( + r'EXTRA_REMOTE_INTENT_TOKEN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_REMOTE_INTENT_TOKEN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_REMOTE_INTENT_TOKEN => + _id_EXTRA_REMOTE_INTENT_TOKEN.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_REPLACEMENT_EXTRAS = _class.staticFieldId( + r'EXTRA_REPLACEMENT_EXTRAS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_REPLACEMENT_EXTRAS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_REPLACEMENT_EXTRAS => + _id_EXTRA_REPLACEMENT_EXTRAS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_REPLACING = _class.staticFieldId( + r'EXTRA_REPLACING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_REPLACING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_REPLACING => + _id_EXTRA_REPLACING.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_RESTRICTIONS_BUNDLE = _class.staticFieldId( + r'EXTRA_RESTRICTIONS_BUNDLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_RESTRICTIONS_BUNDLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_RESTRICTIONS_BUNDLE => + _id_EXTRA_RESTRICTIONS_BUNDLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_RESTRICTIONS_INTENT = _class.staticFieldId( + r'EXTRA_RESTRICTIONS_INTENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_RESTRICTIONS_INTENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_RESTRICTIONS_INTENT => + _id_EXTRA_RESTRICTIONS_INTENT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_RESTRICTIONS_LIST = _class.staticFieldId( + r'EXTRA_RESTRICTIONS_LIST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_RESTRICTIONS_LIST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_RESTRICTIONS_LIST => + _id_EXTRA_RESTRICTIONS_LIST.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_RESULT_RECEIVER = _class.staticFieldId( + r'EXTRA_RESULT_RECEIVER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_RESULT_RECEIVER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_RESULT_RECEIVER => _id_EXTRA_RESULT_RECEIVER + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_RETURN_RESULT = _class.staticFieldId( + r'EXTRA_RETURN_RESULT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_RETURN_RESULT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_RETURN_RESULT => + _id_EXTRA_RETURN_RESULT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHORTCUT_ICON = _class.staticFieldId( + r'EXTRA_SHORTCUT_ICON', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_SHORTCUT_ICON` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_SHORTCUT_ICON => + _id_EXTRA_SHORTCUT_ICON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHORTCUT_ICON_RESOURCE = _class.staticFieldId( + r'EXTRA_SHORTCUT_ICON_RESOURCE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_SHORTCUT_ICON_RESOURCE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_SHORTCUT_ICON_RESOURCE => + _id_EXTRA_SHORTCUT_ICON_RESOURCE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_SHORTCUT_ID = _class.staticFieldId( + r'EXTRA_SHORTCUT_ID', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_SHORTCUT_ID` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_SHORTCUT_ID => + _id_EXTRA_SHORTCUT_ID.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHORTCUT_INTENT = _class.staticFieldId( + r'EXTRA_SHORTCUT_INTENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_SHORTCUT_INTENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_SHORTCUT_INTENT => _id_EXTRA_SHORTCUT_INTENT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHORTCUT_NAME = _class.staticFieldId( + r'EXTRA_SHORTCUT_NAME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_SHORTCUT_NAME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_SHORTCUT_NAME => + _id_EXTRA_SHORTCUT_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SHUTDOWN_USERSPACE_ONLY = _class.staticFieldId( + r'EXTRA_SHUTDOWN_USERSPACE_ONLY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_SHUTDOWN_USERSPACE_ONLY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_SHUTDOWN_USERSPACE_ONLY => + _id_EXTRA_SHUTDOWN_USERSPACE_ONLY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_SPLIT_NAME = _class.staticFieldId( + r'EXTRA_SPLIT_NAME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_SPLIT_NAME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_SPLIT_NAME => + _id_EXTRA_SPLIT_NAME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_START_TIME = _class.staticFieldId( + r'EXTRA_START_TIME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_START_TIME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_START_TIME => + _id_EXTRA_START_TIME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_STREAM = _class.staticFieldId( + r'EXTRA_STREAM', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_STREAM` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_STREAM => + _id_EXTRA_STREAM.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SUBJECT = _class.staticFieldId( + r'EXTRA_SUBJECT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_SUBJECT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_SUBJECT => + _id_EXTRA_SUBJECT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_SUSPENDED_PACKAGE_EXTRAS = _class.staticFieldId( + r'EXTRA_SUSPENDED_PACKAGE_EXTRAS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_SUSPENDED_PACKAGE_EXTRAS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_SUSPENDED_PACKAGE_EXTRAS => + _id_EXTRA_SUSPENDED_PACKAGE_EXTRAS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXTRA_TEMPLATE = _class.staticFieldId( + r'EXTRA_TEMPLATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_TEMPLATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_TEMPLATE => + _id_EXTRA_TEMPLATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_TEXT = _class.staticFieldId( + r'EXTRA_TEXT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_TEXT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_TEXT => + _id_EXTRA_TEXT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_TIME = _class.staticFieldId( + r'EXTRA_TIME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_TIME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_TIME => + _id_EXTRA_TIME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_TIMEZONE = _class.staticFieldId( + r'EXTRA_TIMEZONE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_TIMEZONE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_TIMEZONE => + _id_EXTRA_TIMEZONE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_TITLE = _class.staticFieldId( + r'EXTRA_TITLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_TITLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_TITLE => + _id_EXTRA_TITLE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_UID = _class.staticFieldId( + r'EXTRA_UID', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_UID` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_UID => + _id_EXTRA_UID.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_USER = _class.staticFieldId( + r'EXTRA_USER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_USER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_USER => + _id_EXTRA_USER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_USER_INITIATED = _class.staticFieldId( + r'EXTRA_USER_INITIATED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_USER_INITIATED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_USER_INITIATED => _id_EXTRA_USER_INITIATED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_USE_STYLUS_MODE = _class.staticFieldId( + r'EXTRA_USE_STYLUS_MODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_USE_STYLUS_MODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_USE_STYLUS_MODE => _id_EXTRA_USE_STYLUS_MODE + .get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int FILL_IN_ACTION` + static const FILL_IN_ACTION = 1; + + /// from: `static public final int FILL_IN_CATEGORIES` + static const FILL_IN_CATEGORIES = 4; + + /// from: `static public final int FILL_IN_CLIP_DATA` + static const FILL_IN_CLIP_DATA = 128; + + /// from: `static public final int FILL_IN_COMPONENT` + static const FILL_IN_COMPONENT = 8; + + /// from: `static public final int FILL_IN_DATA` + static const FILL_IN_DATA = 2; + + /// from: `static public final int FILL_IN_IDENTIFIER` + static const FILL_IN_IDENTIFIER = 256; + + /// from: `static public final int FILL_IN_PACKAGE` + static const FILL_IN_PACKAGE = 16; + + /// from: `static public final int FILL_IN_SELECTOR` + static const FILL_IN_SELECTOR = 64; + + /// from: `static public final int FILL_IN_SOURCE_BOUNDS` + static const FILL_IN_SOURCE_BOUNDS = 32; + + /// from: `static public final int FLAG_ACTIVITY_BROUGHT_TO_FRONT` + static const FLAG_ACTIVITY_BROUGHT_TO_FRONT = 4194304; + + /// from: `static public final int FLAG_ACTIVITY_CLEAR_TASK` + static const FLAG_ACTIVITY_CLEAR_TASK = 32768; + + /// from: `static public final int FLAG_ACTIVITY_CLEAR_TOP` + static const FLAG_ACTIVITY_CLEAR_TOP = 67108864; + + /// from: `static public final int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET` + static const FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 524288; + + /// from: `static public final int FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS` + static const FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS = 8388608; + + /// from: `static public final int FLAG_ACTIVITY_FORWARD_RESULT` + static const FLAG_ACTIVITY_FORWARD_RESULT = 33554432; + + /// from: `static public final int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY` + static const FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 1048576; + + /// from: `static public final int FLAG_ACTIVITY_LAUNCH_ADJACENT` + static const FLAG_ACTIVITY_LAUNCH_ADJACENT = 4096; + + /// from: `static public final int FLAG_ACTIVITY_MATCH_EXTERNAL` + static const FLAG_ACTIVITY_MATCH_EXTERNAL = 2048; + + /// from: `static public final int FLAG_ACTIVITY_MULTIPLE_TASK` + static const FLAG_ACTIVITY_MULTIPLE_TASK = 134217728; + + /// from: `static public final int FLAG_ACTIVITY_NEW_DOCUMENT` + static const FLAG_ACTIVITY_NEW_DOCUMENT = 524288; + + /// from: `static public final int FLAG_ACTIVITY_NEW_TASK` + static const FLAG_ACTIVITY_NEW_TASK = 268435456; + + /// from: `static public final int FLAG_ACTIVITY_NO_ANIMATION` + static const FLAG_ACTIVITY_NO_ANIMATION = 65536; + + /// from: `static public final int FLAG_ACTIVITY_NO_HISTORY` + static const FLAG_ACTIVITY_NO_HISTORY = 1073741824; + + /// from: `static public final int FLAG_ACTIVITY_NO_USER_ACTION` + static const FLAG_ACTIVITY_NO_USER_ACTION = 262144; + + /// from: `static public final int FLAG_ACTIVITY_PREVIOUS_IS_TOP` + static const FLAG_ACTIVITY_PREVIOUS_IS_TOP = 16777216; + + /// from: `static public final int FLAG_ACTIVITY_REORDER_TO_FRONT` + static const FLAG_ACTIVITY_REORDER_TO_FRONT = 131072; + + /// from: `static public final int FLAG_ACTIVITY_REQUIRE_DEFAULT` + static const FLAG_ACTIVITY_REQUIRE_DEFAULT = 512; + + /// from: `static public final int FLAG_ACTIVITY_REQUIRE_NON_BROWSER` + static const FLAG_ACTIVITY_REQUIRE_NON_BROWSER = 1024; + + /// from: `static public final int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED` + static const FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 2097152; + + /// from: `static public final int FLAG_ACTIVITY_RETAIN_IN_RECENTS` + static const FLAG_ACTIVITY_RETAIN_IN_RECENTS = 8192; + + /// from: `static public final int FLAG_ACTIVITY_SINGLE_TOP` + static const FLAG_ACTIVITY_SINGLE_TOP = 536870912; + + /// from: `static public final int FLAG_ACTIVITY_TASK_ON_HOME` + static const FLAG_ACTIVITY_TASK_ON_HOME = 16384; + + /// from: `static public final int FLAG_DEBUG_LOG_RESOLUTION` + static const FLAG_DEBUG_LOG_RESOLUTION = 8; + + /// from: `static public final int FLAG_DIRECT_BOOT_AUTO` + static const FLAG_DIRECT_BOOT_AUTO = 256; + + /// from: `static public final int FLAG_EXCLUDE_STOPPED_PACKAGES` + static const FLAG_EXCLUDE_STOPPED_PACKAGES = 16; + + /// from: `static public final int FLAG_FROM_BACKGROUND` + static const FLAG_FROM_BACKGROUND = 4; + + /// from: `static public final int FLAG_GRANT_PERSISTABLE_URI_PERMISSION` + static const FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 64; + + /// from: `static public final int FLAG_GRANT_PREFIX_URI_PERMISSION` + static const FLAG_GRANT_PREFIX_URI_PERMISSION = 128; + + /// from: `static public final int FLAG_GRANT_READ_URI_PERMISSION` + static const FLAG_GRANT_READ_URI_PERMISSION = 1; + + /// from: `static public final int FLAG_GRANT_WRITE_URI_PERMISSION` + static const FLAG_GRANT_WRITE_URI_PERMISSION = 2; + + /// from: `static public final int FLAG_INCLUDE_STOPPED_PACKAGES` + static const FLAG_INCLUDE_STOPPED_PACKAGES = 32; + + /// from: `static public final int FLAG_RECEIVER_FOREGROUND` + static const FLAG_RECEIVER_FOREGROUND = 268435456; + + /// from: `static public final int FLAG_RECEIVER_NO_ABORT` + static const FLAG_RECEIVER_NO_ABORT = 134217728; + + /// from: `static public final int FLAG_RECEIVER_REGISTERED_ONLY` + static const FLAG_RECEIVER_REGISTERED_ONLY = 1073741824; + + /// from: `static public final int FLAG_RECEIVER_REPLACE_PENDING` + static const FLAG_RECEIVER_REPLACE_PENDING = 536870912; + + /// from: `static public final int FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS` + static const FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS = 2097152; + static final _id_METADATA_DOCK_HOME = _class.staticFieldId( + r'METADATA_DOCK_HOME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String METADATA_DOCK_HOME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get METADATA_DOCK_HOME => + _id_METADATA_DOCK_HOME.get(_class, const jni$_.$JString$NullableType$()); + + /// from: `static public final int URI_ALLOW_UNSAFE` + static const URI_ALLOW_UNSAFE = 4; + + /// from: `static public final int URI_ANDROID_APP_SCHEME` + static const URI_ANDROID_APP_SCHEME = 2; + + /// from: `static public final int URI_INTENT_SCHEME` + static const URI_INTENT_SCHEME = 1; + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory Intent() { + return Intent.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } + + static final _id_new$1 = _class.constructorId( + r'(Landroid/content/Context;Ljava/lang/Class;)V', + ); + + static final _new$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void (android.content.Context context, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$1(jni$_.JObject? context, jni$_.JObject? class$) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + _new$1( + _class.reference.pointer, + _id_new$1 as jni$_.JMethodIDPtr, + _$context.pointer, + _$class$.pointer, + ).reference, + ); + } + + static final _id_new$2 = _class.constructorId(r'(Landroid/content/Intent;)V'); + + static final _new$2 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void (android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$2(Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + _new$2( + _class.reference.pointer, + _id_new$2 as jni$_.JMethodIDPtr, + _$intent.pointer, + ).reference, + ); + } + + static final _id_new$3 = _class.constructorId(r'(Ljava/lang/String;)V'); + + static final _new$3 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void (java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$3(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + _new$3( + _class.reference.pointer, + _id_new$3 as jni$_.JMethodIDPtr, + _$string.pointer, + ).reference, + ); + } + + static final _id_new$4 = _class.constructorId( + r'(Ljava/lang/String;Landroid/net/Uri;)V', + ); + + static final _new$4 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void (java.lang.String string, android.net.Uri uri)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$4(jni$_.JString? string, jni$_.JObject? uri) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$uri = uri?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + _new$4( + _class.reference.pointer, + _id_new$4 as jni$_.JMethodIDPtr, + _$string.pointer, + _$uri.pointer, + ).reference, + ); + } + + static final _id_new$5 = _class.constructorId( + r'(Ljava/lang/String;Landroid/net/Uri;Landroid/content/Context;Ljava/lang/Class;)V', + ); + + static final _new$5 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void (java.lang.String string, android.net.Uri uri, android.content.Context context, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + factory Intent.new$5( + jni$_.JString? string, + jni$_.JObject? uri, + jni$_.JObject? context, + jni$_.JObject? class$, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$context = context?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return Intent.fromReference( + _new$5( + _class.reference.pointer, + _id_new$5 as jni$_.JMethodIDPtr, + _$string.pointer, + _$uri.pointer, + _$context.pointer, + _$class$.pointer, + ).reference, + ); + } + + static final _id_addCategory = _class.instanceMethodId( + r'addCategory', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _addCategory = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent addCategory(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? addCategory(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _addCategory( + reference.pointer, + _id_addCategory as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_addFlags = _class.instanceMethodId( + r'addFlags', + r'(I)Landroid/content/Intent;', + ); + + static final _addFlags = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public android.content.Intent addFlags(int i)` + /// The returned object must be released after use, by calling the [release] method. + Intent? addFlags(int i) { + return _addFlags( + reference.pointer, + _id_addFlags as jni$_.JMethodIDPtr, + i, + ).object(const $Intent$NullableType$()); + } + + static final _id_clone = _class.instanceMethodId( + r'clone', + r'()Ljava/lang/Object;', + ); + + static final _clone = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.Object clone()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? clone() { + return _clone( + reference.pointer, + _id_clone as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_cloneFilter = _class.instanceMethodId( + r'cloneFilter', + r'()Landroid/content/Intent;', + ); + + static final _cloneFilter = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.content.Intent cloneFilter()` + /// The returned object must be released after use, by calling the [release] method. + Intent? cloneFilter() { + return _cloneFilter( + reference.pointer, + _id_cloneFilter as jni$_.JMethodIDPtr, + ).object(const $Intent$NullableType$()); + } + + static final _id_createChooser = _class.staticMethodId( + r'createChooser', + r'(Landroid/content/Intent;Ljava/lang/CharSequence;)Landroid/content/Intent;', + ); + + static final _createChooser = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Intent createChooser(android.content.Intent intent, java.lang.CharSequence charSequence)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? createChooser(Intent? intent, jni$_.JObject? charSequence) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + return _createChooser( + _class.reference.pointer, + _id_createChooser as jni$_.JMethodIDPtr, + _$intent.pointer, + _$charSequence.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_createChooser$1 = _class.staticMethodId( + r'createChooser', + r'(Landroid/content/Intent;Ljava/lang/CharSequence;Landroid/content/IntentSender;)Landroid/content/Intent;', + ); + + static final _createChooser$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Intent createChooser(android.content.Intent intent, java.lang.CharSequence charSequence, android.content.IntentSender intentSender)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? createChooser$1( + Intent? intent, + jni$_.JObject? charSequence, + jni$_.JObject? intentSender, + ) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + final _$intentSender = intentSender?.reference ?? jni$_.jNullReference; + return _createChooser$1( + _class.reference.pointer, + _id_createChooser$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + _$charSequence.pointer, + _$intentSender.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_describeContents = _class.instanceMethodId( + r'describeContents', + r'()I', + ); + + static final _describeContents = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int describeContents()` + int describeContents() { + return _describeContents( + reference.pointer, + _id_describeContents as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_fillIn = _class.instanceMethodId( + r'fillIn', + r'(Landroid/content/Intent;I)I', + ); + + static final _fillIn = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public int fillIn(android.content.Intent intent, int i)` + int fillIn(Intent? intent, int i) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _fillIn( + reference.pointer, + _id_fillIn as jni$_.JMethodIDPtr, + _$intent.pointer, + i, + ).integer; + } + + static final _id_filterEquals = _class.instanceMethodId( + r'filterEquals', + r'(Landroid/content/Intent;)Z', + ); + + static final _filterEquals = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean filterEquals(android.content.Intent intent)` + bool filterEquals(Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _filterEquals( + reference.pointer, + _id_filterEquals as jni$_.JMethodIDPtr, + _$intent.pointer, + ).boolean; + } + + static final _id_filterHashCode = _class.instanceMethodId( + r'filterHashCode', + r'()I', + ); + + static final _filterHashCode = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int filterHashCode()` + int filterHashCode() { + return _filterHashCode( + reference.pointer, + _id_filterHashCode as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_getAction = _class.instanceMethodId( + r'getAction', + r'()Ljava/lang/String;', + ); + + static final _getAction = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getAction()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getAction() { + return _getAction( + reference.pointer, + _id_getAction as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getBooleanArrayExtra = _class.instanceMethodId( + r'getBooleanArrayExtra', + r'(Ljava/lang/String;)[Z', + ); + + static final _getBooleanArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean[] getBooleanArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JBooleanArray? getBooleanArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getBooleanArrayExtra( + reference.pointer, + _id_getBooleanArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JBooleanArray$NullableType$()); + } + + static final _id_getBooleanExtra = _class.instanceMethodId( + r'getBooleanExtra', + r'(Ljava/lang/String;Z)Z', + ); + + static final _getBooleanExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public boolean getBooleanExtra(java.lang.String string, boolean z)` + bool getBooleanExtra(jni$_.JString? string, bool z) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getBooleanExtra( + reference.pointer, + _id_getBooleanExtra as jni$_.JMethodIDPtr, + _$string.pointer, + z ? 1 : 0, + ).boolean; + } + + static final _id_getBundleExtra = _class.instanceMethodId( + r'getBundleExtra', + r'(Ljava/lang/String;)Landroid/os/Bundle;', + ); + + static final _getBundleExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.os.Bundle getBundleExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getBundleExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getBundleExtra( + reference.pointer, + _id_getBundleExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getByteArrayExtra = _class.instanceMethodId( + r'getByteArrayExtra', + r'(Ljava/lang/String;)[B', + ); + + static final _getByteArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public byte[] getByteArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JByteArray? getByteArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getByteArrayExtra( + reference.pointer, + _id_getByteArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JByteArray$NullableType$()); + } + + static final _id_getByteExtra = _class.instanceMethodId( + r'getByteExtra', + r'(Ljava/lang/String;B)B', + ); + + static final _getByteExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallByteMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public byte getByteExtra(java.lang.String string, byte b)` + int getByteExtra(jni$_.JString? string, int b) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getByteExtra( + reference.pointer, + _id_getByteExtra as jni$_.JMethodIDPtr, + _$string.pointer, + b, + ).byte; + } + + static final _id_getCategories = _class.instanceMethodId( + r'getCategories', + r'()Ljava/util/Set;', + ); + + static final _getCategories = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.util.Set getCategories()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JSet? getCategories() { + return _getCategories( + reference.pointer, + _id_getCategories as jni$_.JMethodIDPtr, + ).object?>( + const jni$_.$JSet$NullableType$( + jni$_.$JString$NullableType$(), + ), + ); + } + + static final _id_getCharArrayExtra = _class.instanceMethodId( + r'getCharArrayExtra', + r'(Ljava/lang/String;)[C', + ); + + static final _getCharArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public char[] getCharArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JCharArray? getCharArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharArrayExtra( + reference.pointer, + _id_getCharArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JCharArray$NullableType$()); + } + + static final _id_getCharExtra = _class.instanceMethodId( + r'getCharExtra', + r'(Ljava/lang/String;C)C', + ); + + static final _getCharExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallCharMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public char getCharExtra(java.lang.String string, char c)` + int getCharExtra(jni$_.JString? string, int c) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharExtra( + reference.pointer, + _id_getCharExtra as jni$_.JMethodIDPtr, + _$string.pointer, + c, + ).char; + } + + static final _id_getCharSequenceArrayExtra = _class.instanceMethodId( + r'getCharSequenceArrayExtra', + r'(Ljava/lang/String;)[Ljava/lang/CharSequence;', + ); + + static final _getCharSequenceArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.lang.CharSequence[] getCharSequenceArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getCharSequenceArrayExtra( + jni$_.JString? string, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharSequenceArrayExtra( + reference.pointer, + _id_getCharSequenceArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getCharSequenceArrayListExtra = _class.instanceMethodId( + r'getCharSequenceArrayListExtra', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getCharSequenceArrayListExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.ArrayList getCharSequenceArrayListExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCharSequenceArrayListExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharSequenceArrayListExtra( + reference.pointer, + _id_getCharSequenceArrayListExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getCharSequenceExtra = _class.instanceMethodId( + r'getCharSequenceExtra', + r'(Ljava/lang/String;)Ljava/lang/CharSequence;', + ); + + static final _getCharSequenceExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.lang.CharSequence getCharSequenceExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getCharSequenceExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getCharSequenceExtra( + reference.pointer, + _id_getCharSequenceExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getClipData = _class.instanceMethodId( + r'getClipData', + r'()Landroid/content/ClipData;', + ); + + static final _getClipData = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.content.ClipData getClipData()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getClipData() { + return _getClipData( + reference.pointer, + _id_getClipData as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getComponent = _class.instanceMethodId( + r'getComponent', + r'()Landroid/content/ComponentName;', + ); + + static final _getComponent = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.content.ComponentName getComponent()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getComponent() { + return _getComponent( + reference.pointer, + _id_getComponent as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getData = _class.instanceMethodId( + r'getData', + r'()Landroid/net/Uri;', + ); + + static final _getData = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.net.Uri getData()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getData() { + return _getData( + reference.pointer, + _id_getData as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getDataString = _class.instanceMethodId( + r'getDataString', + r'()Ljava/lang/String;', + ); + + static final _getDataString = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getDataString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getDataString() { + return _getDataString( + reference.pointer, + _id_getDataString as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getDoubleArrayExtra = _class.instanceMethodId( + r'getDoubleArrayExtra', + r'(Ljava/lang/String;)[D', + ); + + static final _getDoubleArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public double[] getDoubleArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JDoubleArray? getDoubleArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getDoubleArrayExtra( + reference.pointer, + _id_getDoubleArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JDoubleArray$NullableType$()); + } + + static final _id_getDoubleExtra = _class.instanceMethodId( + r'getDoubleExtra', + r'(Ljava/lang/String;D)D', + ); + + static final _getDoubleExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>, + ) + > + >('globalEnv_CallDoubleMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + double, + ) + >(); + + /// from: `public double getDoubleExtra(java.lang.String string, double d)` + double getDoubleExtra(jni$_.JString? string, double d) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getDoubleExtra( + reference.pointer, + _id_getDoubleExtra as jni$_.JMethodIDPtr, + _$string.pointer, + d, + ).doubleFloat; + } + + static final _id_getExtras = _class.instanceMethodId( + r'getExtras', + r'()Landroid/os/Bundle;', + ); + + static final _getExtras = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.os.Bundle getExtras()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getExtras() { + return _getExtras( + reference.pointer, + _id_getExtras as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getFlags = _class.instanceMethodId(r'getFlags', r'()I'); + + static final _getFlags = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int getFlags()` + int getFlags() { + return _getFlags( + reference.pointer, + _id_getFlags as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_getFloatArrayExtra = _class.instanceMethodId( + r'getFloatArrayExtra', + r'(Ljava/lang/String;)[F', + ); + + static final _getFloatArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public float[] getFloatArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JFloatArray? getFloatArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getFloatArrayExtra( + reference.pointer, + _id_getFloatArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JFloatArray$NullableType$()); + } + + static final _id_getFloatExtra = _class.instanceMethodId( + r'getFloatExtra', + r'(Ljava/lang/String;F)F', + ); + + static final _getFloatExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>, + ) + > + >('globalEnv_CallFloatMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + double, + ) + >(); + + /// from: `public float getFloatExtra(java.lang.String string, float f)` + double getFloatExtra(jni$_.JString? string, double f) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getFloatExtra( + reference.pointer, + _id_getFloatExtra as jni$_.JMethodIDPtr, + _$string.pointer, + f, + ).float; + } + + static final _id_getIdentifier = _class.instanceMethodId( + r'getIdentifier', + r'()Ljava/lang/String;', + ); + + static final _getIdentifier = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getIdentifier()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getIdentifier() { + return _getIdentifier( + reference.pointer, + _id_getIdentifier as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getIntArrayExtra = _class.instanceMethodId( + r'getIntArrayExtra', + r'(Ljava/lang/String;)[I', + ); + + static final _getIntArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public int[] getIntArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JIntArray? getIntArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntArrayExtra( + reference.pointer, + _id_getIntArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JIntArray$NullableType$()); + } + + static final _id_getIntExtra = _class.instanceMethodId( + r'getIntExtra', + r'(Ljava/lang/String;I)I', + ); + + static final _getIntExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public int getIntExtra(java.lang.String string, int i)` + int getIntExtra(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntExtra( + reference.pointer, + _id_getIntExtra as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).integer; + } + + static final _id_getIntegerArrayListExtra = _class.instanceMethodId( + r'getIntegerArrayListExtra', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getIntegerArrayListExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.ArrayList getIntegerArrayListExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getIntegerArrayListExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntegerArrayListExtra( + reference.pointer, + _id_getIntegerArrayListExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getIntent = _class.staticMethodId( + r'getIntent', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _getIntent = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Intent getIntent(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? getIntent(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntent( + _class.reference.pointer, + _id_getIntent as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_getIntentOld = _class.staticMethodId( + r'getIntentOld', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _getIntentOld = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Intent getIntentOld(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? getIntentOld(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getIntentOld( + _class.reference.pointer, + _id_getIntentOld as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_getLongArrayExtra = _class.instanceMethodId( + r'getLongArrayExtra', + r'(Ljava/lang/String;)[J', + ); + + static final _getLongArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public long[] getLongArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JLongArray? getLongArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getLongArrayExtra( + reference.pointer, + _id_getLongArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JLongArray$NullableType$()); + } + + static final _id_getLongExtra = _class.instanceMethodId( + r'getLongExtra', + r'(Ljava/lang/String;J)J', + ); + + static final _getLongExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int64)>, + ) + > + >('globalEnv_CallLongMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public long getLongExtra(java.lang.String string, long j)` + int getLongExtra(jni$_.JString? string, int j) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getLongExtra( + reference.pointer, + _id_getLongExtra as jni$_.JMethodIDPtr, + _$string.pointer, + j, + ).long; + } + + static final _id_getPackage = _class.instanceMethodId( + r'getPackage', + r'()Ljava/lang/String;', + ); + + static final _getPackage = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getPackage()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getPackage() { + return _getPackage( + reference.pointer, + _id_getPackage as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getParcelableArrayExtra = _class.instanceMethodId( + r'getParcelableArrayExtra', + r'(Ljava/lang/String;)[Landroid/os/Parcelable;', + ); + + static final _getParcelableArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.os.Parcelable[] getParcelableArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getParcelableArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getParcelableArrayExtra( + reference.pointer, + _id_getParcelableArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getParcelableArrayExtra$1 = _class.instanceMethodId( + r'getParcelableArrayExtra', + r'(Ljava/lang/String;Ljava/lang/Class;)[Ljava/lang/Object;', + ); + + static final _getParcelableArrayExtra$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public T[] getParcelableArrayExtra(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray<$T?>? getParcelableArrayExtra$1<$T extends jni$_.JObject?>( + jni$_.JString? string, + jni$_.JObject? class$, { + required jni$_.JType<$T> T, + }) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getParcelableArrayExtra$1( + reference.pointer, + _id_getParcelableArrayExtra$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$class$.pointer, + ).object?>( + jni$_.$JArray$NullableType$<$T?>(T.nullableType), + ); + } + + static final _id_getParcelableArrayListExtra = _class.instanceMethodId( + r'getParcelableArrayListExtra', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getParcelableArrayListExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.ArrayList getParcelableArrayListExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getParcelableArrayListExtra<$T extends jni$_.JObject?>( + jni$_.JString? string, { + required jni$_.JType<$T> T, + }) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getParcelableArrayListExtra( + reference.pointer, + _id_getParcelableArrayListExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getParcelableArrayListExtra$1 = _class.instanceMethodId( + r'getParcelableArrayListExtra', + r'(Ljava/lang/String;Ljava/lang/Class;)Ljava/util/ArrayList;', + ); + + static final _getParcelableArrayListExtra$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.ArrayList getParcelableArrayListExtra(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getParcelableArrayListExtra$1<$T extends jni$_.JObject?>( + jni$_.JString? string, + jni$_.JObject? class$, { + required jni$_.JType<$T> T, + }) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getParcelableArrayListExtra$1( + reference.pointer, + _id_getParcelableArrayListExtra$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$class$.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getParcelableExtra = _class.instanceMethodId( + r'getParcelableExtra', + r'(Ljava/lang/String;)Landroid/os/Parcelable;', + ); + + static final _getParcelableExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public T getParcelableExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + $T? getParcelableExtra<$T extends jni$_.JObject?>( + jni$_.JString? string, { + required jni$_.JType<$T> T, + }) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getParcelableExtra( + reference.pointer, + _id_getParcelableExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object<$T?>(T.nullableType); + } + + static final _id_getParcelableExtra$1 = _class.instanceMethodId( + r'getParcelableExtra', + r'(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;', + ); + + static final _getParcelableExtra$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public T getParcelableExtra(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + $T? getParcelableExtra$1<$T extends jni$_.JObject?>( + jni$_.JString? string, + jni$_.JObject? class$, { + required jni$_.JType<$T> T, + }) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getParcelableExtra$1( + reference.pointer, + _id_getParcelableExtra$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$class$.pointer, + ).object<$T?>(T.nullableType); + } + + static final _id_getScheme = _class.instanceMethodId( + r'getScheme', + r'()Ljava/lang/String;', + ); + + static final _getScheme = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getScheme()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getScheme() { + return _getScheme( + reference.pointer, + _id_getScheme as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getSelector = _class.instanceMethodId( + r'getSelector', + r'()Landroid/content/Intent;', + ); + + static final _getSelector = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.content.Intent getSelector()` + /// The returned object must be released after use, by calling the [release] method. + Intent? getSelector() { + return _getSelector( + reference.pointer, + _id_getSelector as jni$_.JMethodIDPtr, + ).object(const $Intent$NullableType$()); + } + + static final _id_getSerializableExtra = _class.instanceMethodId( + r'getSerializableExtra', + r'(Ljava/lang/String;)Ljava/io/Serializable;', + ); + + static final _getSerializableExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.io.Serializable getSerializableExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSerializableExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getSerializableExtra( + reference.pointer, + _id_getSerializableExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSerializableExtra$1 = _class.instanceMethodId( + r'getSerializableExtra', + r'(Ljava/lang/String;Ljava/lang/Class;)Ljava/io/Serializable;', + ); + + static final _getSerializableExtra$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public T getSerializableExtra(java.lang.String string, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + $T? getSerializableExtra$1<$T extends jni$_.JObject?>( + jni$_.JString? string, + jni$_.JObject? class$, { + required jni$_.JType<$T> T, + }) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getSerializableExtra$1( + reference.pointer, + _id_getSerializableExtra$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$class$.pointer, + ).object<$T?>(T.nullableType); + } + + static final _id_getShortArrayExtra = _class.instanceMethodId( + r'getShortArrayExtra', + r'(Ljava/lang/String;)[S', + ); + + static final _getShortArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public short[] getShortArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JShortArray? getShortArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getShortArrayExtra( + reference.pointer, + _id_getShortArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JShortArray$NullableType$()); + } + + static final _id_getShortExtra = _class.instanceMethodId( + r'getShortExtra', + r'(Ljava/lang/String;S)S', + ); + + static final _getShortExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallShortMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public short getShortExtra(java.lang.String string, short s)` + int getShortExtra(jni$_.JString? string, int s) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getShortExtra( + reference.pointer, + _id_getShortExtra as jni$_.JMethodIDPtr, + _$string.pointer, + s, + ).short; + } + + static final _id_getSourceBounds = _class.instanceMethodId( + r'getSourceBounds', + r'()Landroid/graphics/Rect;', + ); + + static final _getSourceBounds = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.graphics.Rect getSourceBounds()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSourceBounds() { + return _getSourceBounds( + reference.pointer, + _id_getSourceBounds as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getStringArrayExtra = _class.instanceMethodId( + r'getStringArrayExtra', + r'(Ljava/lang/String;)[Ljava/lang/String;', + ); + + static final _getStringArrayExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.lang.String[] getStringArrayExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getStringArrayExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getStringArrayExtra( + reference.pointer, + _id_getStringArrayExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JString$NullableType$(), + ), + ); + } + + static final _id_getStringArrayListExtra = _class.instanceMethodId( + r'getStringArrayListExtra', + r'(Ljava/lang/String;)Ljava/util/ArrayList;', + ); + + static final _getStringArrayListExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.ArrayList getStringArrayListExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getStringArrayListExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getStringArrayListExtra( + reference.pointer, + _id_getStringArrayListExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getStringExtra = _class.instanceMethodId( + r'getStringExtra', + r'(Ljava/lang/String;)Ljava/lang/String;', + ); + + static final _getStringExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.lang.String getStringExtra(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getStringExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getStringExtra( + reference.pointer, + _id_getStringExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getType = _class.instanceMethodId( + r'getType', + r'()Ljava/lang/String;', + ); + + static final _getType = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getType()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getType() { + return _getType( + reference.pointer, + _id_getType as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_hasCategory = _class.instanceMethodId( + r'hasCategory', + r'(Ljava/lang/String;)Z', + ); + + static final _hasCategory = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean hasCategory(java.lang.String string)` + bool hasCategory(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _hasCategory( + reference.pointer, + _id_hasCategory as jni$_.JMethodIDPtr, + _$string.pointer, + ).boolean; + } + + static final _id_hasExtra = _class.instanceMethodId( + r'hasExtra', + r'(Ljava/lang/String;)Z', + ); + + static final _hasExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean hasExtra(java.lang.String string)` + bool hasExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _hasExtra( + reference.pointer, + _id_hasExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).boolean; + } + + static final _id_hasFileDescriptors = _class.instanceMethodId( + r'hasFileDescriptors', + r'()Z', + ); + + static final _hasFileDescriptors = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean hasFileDescriptors()` + bool hasFileDescriptors() { + return _hasFileDescriptors( + reference.pointer, + _id_hasFileDescriptors as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_isMismatchingFilter = _class.instanceMethodId( + r'isMismatchingFilter', + r'()Z', + ); + + static final _isMismatchingFilter = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean isMismatchingFilter()` + bool isMismatchingFilter() { + return _isMismatchingFilter( + reference.pointer, + _id_isMismatchingFilter as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_makeMainActivity = _class.staticMethodId( + r'makeMainActivity', + r'(Landroid/content/ComponentName;)Landroid/content/Intent;', + ); + + static final _makeMainActivity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Intent makeMainActivity(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? makeMainActivity(jni$_.JObject? componentName) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _makeMainActivity( + _class.reference.pointer, + _id_makeMainActivity as jni$_.JMethodIDPtr, + _$componentName.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_makeMainSelectorActivity = _class.staticMethodId( + r'makeMainSelectorActivity', + r'(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _makeMainSelectorActivity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Intent makeMainSelectorActivity(java.lang.String string, java.lang.String string1)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? makeMainSelectorActivity( + jni$_.JString? string, + jni$_.JString? string1, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _makeMainSelectorActivity( + _class.reference.pointer, + _id_makeMainSelectorActivity as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_makeRestartActivityTask = _class.staticMethodId( + r'makeRestartActivityTask', + r'(Landroid/content/ComponentName;)Landroid/content/Intent;', + ); + + static final _makeRestartActivityTask = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Intent makeRestartActivityTask(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? makeRestartActivityTask(jni$_.JObject? componentName) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _makeRestartActivityTask( + _class.reference.pointer, + _id_makeRestartActivityTask as jni$_.JMethodIDPtr, + _$componentName.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_normalizeMimeType = _class.staticMethodId( + r'normalizeMimeType', + r'(Ljava/lang/String;)Ljava/lang/String;', + ); + + static final _normalizeMimeType = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.lang.String normalizeMimeType(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? normalizeMimeType(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _normalizeMimeType( + _class.reference.pointer, + _id_normalizeMimeType as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_parseIntent = _class.staticMethodId( + r'parseIntent', + r'(Landroid/content/res/Resources;Lorg/xmlpull/v1/XmlPullParser;Landroid/util/AttributeSet;)Landroid/content/Intent;', + ); + + static final _parseIntent = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Intent parseIntent(android.content.res.Resources resources, org.xmlpull.v1.XmlPullParser xmlPullParser, android.util.AttributeSet attributeSet)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? parseIntent( + jni$_.JObject? resources, + jni$_.JObject? xmlPullParser, + jni$_.JObject? attributeSet, + ) { + final _$resources = resources?.reference ?? jni$_.jNullReference; + final _$xmlPullParser = xmlPullParser?.reference ?? jni$_.jNullReference; + final _$attributeSet = attributeSet?.reference ?? jni$_.jNullReference; + return _parseIntent( + _class.reference.pointer, + _id_parseIntent as jni$_.JMethodIDPtr, + _$resources.pointer, + _$xmlPullParser.pointer, + _$attributeSet.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_parseUri = _class.staticMethodId( + r'parseUri', + r'(Ljava/lang/String;I)Landroid/content/Intent;', + ); + + static final _parseUri = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public android.content.Intent parseUri(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? parseUri(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _parseUri( + _class.reference.pointer, + _id_parseUri as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const $Intent$NullableType$()); + } + + static final _id_putCharSequenceArrayListExtra = _class.instanceMethodId( + r'putCharSequenceArrayListExtra', + r'(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;', + ); + + static final _putCharSequenceArrayListExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putCharSequenceArrayListExtra(java.lang.String string, java.util.ArrayList arrayList)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putCharSequenceArrayListExtra( + jni$_.JString? string, + jni$_.JObject? arrayList, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + return _putCharSequenceArrayListExtra( + reference.pointer, + _id_putCharSequenceArrayListExtra as jni$_.JMethodIDPtr, + _$string.pointer, + _$arrayList.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Landroid/os/Bundle;)Landroid/content/Intent;', + ); + + static final _putExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, android.os.Bundle bundle)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra(jni$_.JString? string, jni$_.JObject? bundle) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _putExtra( + reference.pointer, + _id_putExtra as jni$_.JMethodIDPtr, + _$string.pointer, + _$bundle.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$1 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;', + ); + + static final _putExtra$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, android.os.Parcelable parcelable)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$1(jni$_.JString? string, jni$_.JObject? parcelable) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$parcelable = parcelable?.reference ?? jni$_.jNullReference; + return _putExtra$1( + reference.pointer, + _id_putExtra$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$parcelable.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$2 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[Landroid/os/Parcelable;)Landroid/content/Intent;', + ); + + static final _putExtra$2 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, android.os.Parcelable[] parcelables)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$2( + jni$_.JString? string, + jni$_.JArray? parcelables, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$parcelables = parcelables?.reference ?? jni$_.jNullReference; + return _putExtra$2( + reference.pointer, + _id_putExtra$2 as jni$_.JMethodIDPtr, + _$string.pointer, + _$parcelables.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$3 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Z)Landroid/content/Intent;', + ); + + static final _putExtra$3 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, boolean z)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$3(jni$_.JString? string, bool z) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$3( + reference.pointer, + _id_putExtra$3 as jni$_.JMethodIDPtr, + _$string.pointer, + z ? 1 : 0, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$4 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[Z)Landroid/content/Intent;', + ); + + static final _putExtra$4 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, boolean[] zs)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$4(jni$_.JString? string, jni$_.JBooleanArray? zs) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$zs = zs?.reference ?? jni$_.jNullReference; + return _putExtra$4( + reference.pointer, + _id_putExtra$4 as jni$_.JMethodIDPtr, + _$string.pointer, + _$zs.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$5 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;B)Landroid/content/Intent;', + ); + + static final _putExtra$5 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, byte b)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$5(jni$_.JString? string, int b) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$5( + reference.pointer, + _id_putExtra$5 as jni$_.JMethodIDPtr, + _$string.pointer, + b, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$6 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[B)Landroid/content/Intent;', + ); + + static final _putExtra$6 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, byte[] bs)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$6(jni$_.JString? string, jni$_.JByteArray? bs) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bs = bs?.reference ?? jni$_.jNullReference; + return _putExtra$6( + reference.pointer, + _id_putExtra$6 as jni$_.JMethodIDPtr, + _$string.pointer, + _$bs.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$7 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;C)Landroid/content/Intent;', + ); + + static final _putExtra$7 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, char c)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$7(jni$_.JString? string, int c) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$7( + reference.pointer, + _id_putExtra$7 as jni$_.JMethodIDPtr, + _$string.pointer, + c, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$8 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[C)Landroid/content/Intent;', + ); + + static final _putExtra$8 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, char[] cs)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$8(jni$_.JString? string, jni$_.JCharArray? cs) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$cs = cs?.reference ?? jni$_.jNullReference; + return _putExtra$8( + reference.pointer, + _id_putExtra$8 as jni$_.JMethodIDPtr, + _$string.pointer, + _$cs.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$9 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;D)Landroid/content/Intent;', + ); + + static final _putExtra$9 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + double, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, double d)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$9(jni$_.JString? string, double d) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$9( + reference.pointer, + _id_putExtra$9 as jni$_.JMethodIDPtr, + _$string.pointer, + d, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$10 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[D)Landroid/content/Intent;', + ); + + static final _putExtra$10 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, double[] ds)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$10(jni$_.JString? string, jni$_.JDoubleArray? ds) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$ds = ds?.reference ?? jni$_.jNullReference; + return _putExtra$10( + reference.pointer, + _id_putExtra$10 as jni$_.JMethodIDPtr, + _$string.pointer, + _$ds.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$11 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;F)Landroid/content/Intent;', + ); + + static final _putExtra$11 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Double)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + double, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, float f)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$11(jni$_.JString? string, double f) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$11( + reference.pointer, + _id_putExtra$11 as jni$_.JMethodIDPtr, + _$string.pointer, + f, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$12 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[F)Landroid/content/Intent;', + ); + + static final _putExtra$12 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, float[] fs)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$12(jni$_.JString? string, jni$_.JFloatArray? fs) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$fs = fs?.reference ?? jni$_.jNullReference; + return _putExtra$12( + reference.pointer, + _id_putExtra$12 as jni$_.JMethodIDPtr, + _$string.pointer, + _$fs.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$13 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;I)Landroid/content/Intent;', + ); + + static final _putExtra$13 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$13(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$13( + reference.pointer, + _id_putExtra$13 as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$14 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[I)Landroid/content/Intent;', + ); + + static final _putExtra$14 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, int[] is)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$14(jni$_.JString? string, jni$_.JIntArray? is$) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$is$ = is$?.reference ?? jni$_.jNullReference; + return _putExtra$14( + reference.pointer, + _id_putExtra$14 as jni$_.JMethodIDPtr, + _$string.pointer, + _$is$.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$15 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Ljava/io/Serializable;)Landroid/content/Intent;', + ); + + static final _putExtra$15 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, java.io.Serializable serializable)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$15(jni$_.JString? string, jni$_.JObject? serializable) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$serializable = serializable?.reference ?? jni$_.jNullReference; + return _putExtra$15( + reference.pointer, + _id_putExtra$15 as jni$_.JMethodIDPtr, + _$string.pointer, + _$serializable.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$16 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Ljava/lang/CharSequence;)Landroid/content/Intent;', + ); + + static final _putExtra$16 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, java.lang.CharSequence charSequence)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$16(jni$_.JString? string, jni$_.JObject? charSequence) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + return _putExtra$16( + reference.pointer, + _id_putExtra$16 as jni$_.JMethodIDPtr, + _$string.pointer, + _$charSequence.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$17 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[Ljava/lang/CharSequence;)Landroid/content/Intent;', + ); + + static final _putExtra$17 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, java.lang.CharSequence[] charSequences)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$17( + jni$_.JString? string, + jni$_.JArray? charSequences, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$charSequences = charSequences?.reference ?? jni$_.jNullReference; + return _putExtra$17( + reference.pointer, + _id_putExtra$17 as jni$_.JMethodIDPtr, + _$string.pointer, + _$charSequences.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$18 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _putExtra$18 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, java.lang.String string1)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$18(jni$_.JString? string, jni$_.JString? string1) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _putExtra$18( + reference.pointer, + _id_putExtra$18 as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$19 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _putExtra$19 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, java.lang.String[] strings)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$19( + jni$_.JString? string, + jni$_.JArray? strings, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$strings = strings?.reference ?? jni$_.jNullReference; + return _putExtra$19( + reference.pointer, + _id_putExtra$19 as jni$_.JMethodIDPtr, + _$string.pointer, + _$strings.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$20 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;J)Landroid/content/Intent;', + ); + + static final _putExtra$20 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int64)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, long j)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$20(jni$_.JString? string, int j) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$20( + reference.pointer, + _id_putExtra$20 as jni$_.JMethodIDPtr, + _$string.pointer, + j, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$21 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[J)Landroid/content/Intent;', + ); + + static final _putExtra$21 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, long[] js)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$21(jni$_.JString? string, jni$_.JLongArray? js) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$js = js?.reference ?? jni$_.jNullReference; + return _putExtra$21( + reference.pointer, + _id_putExtra$21 as jni$_.JMethodIDPtr, + _$string.pointer, + _$js.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$22 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;S)Landroid/content/Intent;', + ); + + static final _putExtra$22 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, short s)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$22(jni$_.JString? string, int s) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _putExtra$22( + reference.pointer, + _id_putExtra$22 as jni$_.JMethodIDPtr, + _$string.pointer, + s, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtra$23 = _class.instanceMethodId( + r'putExtra', + r'(Ljava/lang/String;[S)Landroid/content/Intent;', + ); + + static final _putExtra$23 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtra(java.lang.String string, short[] ss)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtra$23(jni$_.JString? string, jni$_.JShortArray? ss) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$ss = ss?.reference ?? jni$_.jNullReference; + return _putExtra$23( + reference.pointer, + _id_putExtra$23 as jni$_.JMethodIDPtr, + _$string.pointer, + _$ss.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtras = _class.instanceMethodId( + r'putExtras', + r'(Landroid/content/Intent;)Landroid/content/Intent;', + ); + + static final _putExtras = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtras(android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtras(Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _putExtras( + reference.pointer, + _id_putExtras as jni$_.JMethodIDPtr, + _$intent.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putExtras$1 = _class.instanceMethodId( + r'putExtras', + r'(Landroid/os/Bundle;)Landroid/content/Intent;', + ); + + static final _putExtras$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putExtras(android.os.Bundle bundle)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putExtras$1(jni$_.JObject? bundle) { + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _putExtras$1( + reference.pointer, + _id_putExtras$1 as jni$_.JMethodIDPtr, + _$bundle.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putIntegerArrayListExtra = _class.instanceMethodId( + r'putIntegerArrayListExtra', + r'(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;', + ); + + static final _putIntegerArrayListExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putIntegerArrayListExtra(java.lang.String string, java.util.ArrayList arrayList)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putIntegerArrayListExtra( + jni$_.JString? string, + jni$_.JObject? arrayList, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + return _putIntegerArrayListExtra( + reference.pointer, + _id_putIntegerArrayListExtra as jni$_.JMethodIDPtr, + _$string.pointer, + _$arrayList.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putParcelableArrayListExtra = _class.instanceMethodId( + r'putParcelableArrayListExtra', + r'(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;', + ); + + static final _putParcelableArrayListExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putParcelableArrayListExtra(java.lang.String string, java.util.ArrayList arrayList)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putParcelableArrayListExtra( + jni$_.JString? string, + jni$_.JObject? arrayList, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + return _putParcelableArrayListExtra( + reference.pointer, + _id_putParcelableArrayListExtra as jni$_.JMethodIDPtr, + _$string.pointer, + _$arrayList.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_putStringArrayListExtra = _class.instanceMethodId( + r'putStringArrayListExtra', + r'(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;', + ); + + static final _putStringArrayListExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent putStringArrayListExtra(java.lang.String string, java.util.ArrayList arrayList)` + /// The returned object must be released after use, by calling the [release] method. + Intent? putStringArrayListExtra( + jni$_.JString? string, + jni$_.JObject? arrayList, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$arrayList = arrayList?.reference ?? jni$_.jNullReference; + return _putStringArrayListExtra( + reference.pointer, + _id_putStringArrayListExtra as jni$_.JMethodIDPtr, + _$string.pointer, + _$arrayList.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_readFromParcel = _class.instanceMethodId( + r'readFromParcel', + r'(Landroid/os/Parcel;)V', + ); + + static final _readFromParcel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void readFromParcel(android.os.Parcel parcel)` + void readFromParcel(jni$_.JObject? parcel) { + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _readFromParcel( + reference.pointer, + _id_readFromParcel as jni$_.JMethodIDPtr, + _$parcel.pointer, + ).check(); + } + + static final _id_removeCategory = _class.instanceMethodId( + r'removeCategory', + r'(Ljava/lang/String;)V', + ); + + static final _removeCategory = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void removeCategory(java.lang.String string)` + void removeCategory(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + _removeCategory( + reference.pointer, + _id_removeCategory as jni$_.JMethodIDPtr, + _$string.pointer, + ).check(); + } + + static final _id_removeExtra = _class.instanceMethodId( + r'removeExtra', + r'(Ljava/lang/String;)V', + ); + + static final _removeExtra = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void removeExtra(java.lang.String string)` + void removeExtra(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + _removeExtra( + reference.pointer, + _id_removeExtra as jni$_.JMethodIDPtr, + _$string.pointer, + ).check(); + } + + static final _id_removeFlags = _class.instanceMethodId( + r'removeFlags', + r'(I)V', + ); + + static final _removeFlags = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public void removeFlags(int i)` + void removeFlags(int i) { + _removeFlags( + reference.pointer, + _id_removeFlags as jni$_.JMethodIDPtr, + i, + ).check(); + } + + static final _id_removeLaunchSecurityProtection = _class.instanceMethodId( + r'removeLaunchSecurityProtection', + r'()V', + ); + + static final _removeLaunchSecurityProtection = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void removeLaunchSecurityProtection()` + void removeLaunchSecurityProtection() { + _removeLaunchSecurityProtection( + reference.pointer, + _id_removeLaunchSecurityProtection as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_replaceExtras = _class.instanceMethodId( + r'replaceExtras', + r'(Landroid/content/Intent;)Landroid/content/Intent;', + ); + + static final _replaceExtras = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent replaceExtras(android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + Intent? replaceExtras(Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _replaceExtras( + reference.pointer, + _id_replaceExtras as jni$_.JMethodIDPtr, + _$intent.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_replaceExtras$1 = _class.instanceMethodId( + r'replaceExtras', + r'(Landroid/os/Bundle;)Landroid/content/Intent;', + ); + + static final _replaceExtras$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent replaceExtras(android.os.Bundle bundle)` + /// The returned object must be released after use, by calling the [release] method. + Intent? replaceExtras$1(jni$_.JObject? bundle) { + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _replaceExtras$1( + reference.pointer, + _id_replaceExtras$1 as jni$_.JMethodIDPtr, + _$bundle.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_resolveActivity = _class.instanceMethodId( + r'resolveActivity', + r'(Landroid/content/pm/PackageManager;)Landroid/content/ComponentName;', + ); + + static final _resolveActivity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.ComponentName resolveActivity(android.content.pm.PackageManager packageManager)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveActivity(PackageManager? packageManager) { + final _$packageManager = packageManager?.reference ?? jni$_.jNullReference; + return _resolveActivity( + reference.pointer, + _id_resolveActivity as jni$_.JMethodIDPtr, + _$packageManager.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_resolveActivityInfo = _class.instanceMethodId( + r'resolveActivityInfo', + r'(Landroid/content/pm/PackageManager;I)Landroid/content/pm/ActivityInfo;', + ); + + static final _resolveActivityInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public android.content.pm.ActivityInfo resolveActivityInfo(android.content.pm.PackageManager packageManager, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveActivityInfo(PackageManager? packageManager, int i) { + final _$packageManager = packageManager?.reference ?? jni$_.jNullReference; + return _resolveActivityInfo( + reference.pointer, + _id_resolveActivityInfo as jni$_.JMethodIDPtr, + _$packageManager.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_resolveType = _class.instanceMethodId( + r'resolveType', + r'(Landroid/content/ContentResolver;)Ljava/lang/String;', + ); + + static final _resolveType = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.lang.String resolveType(android.content.ContentResolver contentResolver)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? resolveType(jni$_.JObject? contentResolver) { + final _$contentResolver = + contentResolver?.reference ?? jni$_.jNullReference; + return _resolveType( + reference.pointer, + _id_resolveType as jni$_.JMethodIDPtr, + _$contentResolver.pointer, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_resolveType$1 = _class.instanceMethodId( + r'resolveType', + r'(Landroid/content/Context;)Ljava/lang/String;', + ); + + static final _resolveType$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.lang.String resolveType(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? resolveType$1(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _resolveType$1( + reference.pointer, + _id_resolveType$1 as jni$_.JMethodIDPtr, + _$context.pointer, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_resolveTypeIfNeeded = _class.instanceMethodId( + r'resolveTypeIfNeeded', + r'(Landroid/content/ContentResolver;)Ljava/lang/String;', + ); + + static final _resolveTypeIfNeeded = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.lang.String resolveTypeIfNeeded(android.content.ContentResolver contentResolver)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? resolveTypeIfNeeded(jni$_.JObject? contentResolver) { + final _$contentResolver = + contentResolver?.reference ?? jni$_.jNullReference; + return _resolveTypeIfNeeded( + reference.pointer, + _id_resolveTypeIfNeeded as jni$_.JMethodIDPtr, + _$contentResolver.pointer, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_setAction = _class.instanceMethodId( + r'setAction', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setAction = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setAction(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setAction(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _setAction( + reference.pointer, + _id_setAction as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setClass = _class.instanceMethodId( + r'setClass', + r'(Landroid/content/Context;Ljava/lang/Class;)Landroid/content/Intent;', + ); + + static final _setClass = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setClass(android.content.Context context, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setClass(jni$_.JObject? context, jni$_.JObject? class$) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _setClass( + reference.pointer, + _id_setClass as jni$_.JMethodIDPtr, + _$context.pointer, + _$class$.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setClassName = _class.instanceMethodId( + r'setClassName', + r'(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setClassName = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setClassName(android.content.Context context, java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setClassName(jni$_.JObject? context, jni$_.JString? string) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _setClassName( + reference.pointer, + _id_setClassName as jni$_.JMethodIDPtr, + _$context.pointer, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setClassName$1 = _class.instanceMethodId( + r'setClassName', + r'(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setClassName$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setClassName(java.lang.String string, java.lang.String string1)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setClassName$1(jni$_.JString? string, jni$_.JString? string1) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _setClassName$1( + reference.pointer, + _id_setClassName$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setClipData = _class.instanceMethodId( + r'setClipData', + r'(Landroid/content/ClipData;)V', + ); + + static final _setClipData = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void setClipData(android.content.ClipData clipData)` + void setClipData(jni$_.JObject? clipData) { + final _$clipData = clipData?.reference ?? jni$_.jNullReference; + _setClipData( + reference.pointer, + _id_setClipData as jni$_.JMethodIDPtr, + _$clipData.pointer, + ).check(); + } + + static final _id_setComponent = _class.instanceMethodId( + r'setComponent', + r'(Landroid/content/ComponentName;)Landroid/content/Intent;', + ); + + static final _setComponent = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setComponent(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setComponent(jni$_.JObject? componentName) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _setComponent( + reference.pointer, + _id_setComponent as jni$_.JMethodIDPtr, + _$componentName.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setData = _class.instanceMethodId( + r'setData', + r'(Landroid/net/Uri;)Landroid/content/Intent;', + ); + + static final _setData = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setData(android.net.Uri uri)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setData(jni$_.JObject? uri) { + final _$uri = uri?.reference ?? jni$_.jNullReference; + return _setData( + reference.pointer, + _id_setData as jni$_.JMethodIDPtr, + _$uri.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setDataAndNormalize = _class.instanceMethodId( + r'setDataAndNormalize', + r'(Landroid/net/Uri;)Landroid/content/Intent;', + ); + + static final _setDataAndNormalize = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setDataAndNormalize(android.net.Uri uri)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setDataAndNormalize(jni$_.JObject? uri) { + final _$uri = uri?.reference ?? jni$_.jNullReference; + return _setDataAndNormalize( + reference.pointer, + _id_setDataAndNormalize as jni$_.JMethodIDPtr, + _$uri.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setDataAndType = _class.instanceMethodId( + r'setDataAndType', + r'(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setDataAndType = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setDataAndType(android.net.Uri uri, java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setDataAndType(jni$_.JObject? uri, jni$_.JString? string) { + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _setDataAndType( + reference.pointer, + _id_setDataAndType as jni$_.JMethodIDPtr, + _$uri.pointer, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setDataAndTypeAndNormalize = _class.instanceMethodId( + r'setDataAndTypeAndNormalize', + r'(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setDataAndTypeAndNormalize = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setDataAndTypeAndNormalize(android.net.Uri uri, java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setDataAndTypeAndNormalize( + jni$_.JObject? uri, + jni$_.JString? string, + ) { + final _$uri = uri?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _setDataAndTypeAndNormalize( + reference.pointer, + _id_setDataAndTypeAndNormalize as jni$_.JMethodIDPtr, + _$uri.pointer, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setExtrasClassLoader = _class.instanceMethodId( + r'setExtrasClassLoader', + r'(Ljava/lang/ClassLoader;)V', + ); + + static final _setExtrasClassLoader = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void setExtrasClassLoader(java.lang.ClassLoader classLoader)` + void setExtrasClassLoader(jni$_.JObject? classLoader) { + final _$classLoader = classLoader?.reference ?? jni$_.jNullReference; + _setExtrasClassLoader( + reference.pointer, + _id_setExtrasClassLoader as jni$_.JMethodIDPtr, + _$classLoader.pointer, + ).check(); + } + + static final _id_setFlags = _class.instanceMethodId( + r'setFlags', + r'(I)Landroid/content/Intent;', + ); + + static final _setFlags = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public android.content.Intent setFlags(int i)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setFlags(int i) { + return _setFlags( + reference.pointer, + _id_setFlags as jni$_.JMethodIDPtr, + i, + ).object(const $Intent$NullableType$()); + } + + static final _id_setIdentifier = _class.instanceMethodId( + r'setIdentifier', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setIdentifier = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setIdentifier(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setIdentifier(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _setIdentifier( + reference.pointer, + _id_setIdentifier as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setPackage = _class.instanceMethodId( + r'setPackage', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setPackage = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setPackage(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setPackage(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _setPackage( + reference.pointer, + _id_setPackage as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setSelector = _class.instanceMethodId( + r'setSelector', + r'(Landroid/content/Intent;)V', + ); + + static final _setSelector = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void setSelector(android.content.Intent intent)` + void setSelector(Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + _setSelector( + reference.pointer, + _id_setSelector as jni$_.JMethodIDPtr, + _$intent.pointer, + ).check(); + } + + static final _id_setSourceBounds = _class.instanceMethodId( + r'setSourceBounds', + r'(Landroid/graphics/Rect;)V', + ); + + static final _setSourceBounds = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void setSourceBounds(android.graphics.Rect rect)` + void setSourceBounds(jni$_.JObject? rect) { + final _$rect = rect?.reference ?? jni$_.jNullReference; + _setSourceBounds( + reference.pointer, + _id_setSourceBounds as jni$_.JMethodIDPtr, + _$rect.pointer, + ).check(); + } + + static final _id_setType = _class.instanceMethodId( + r'setType', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setType = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setType(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setType(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _setType( + reference.pointer, + _id_setType as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_setTypeAndNormalize = _class.instanceMethodId( + r'setTypeAndNormalize', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _setTypeAndNormalize = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.Intent setTypeAndNormalize(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? setTypeAndNormalize(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _setTypeAndNormalize( + reference.pointer, + _id_setTypeAndNormalize as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_toString$1 = _class.instanceMethodId( + r'toString', + r'()Ljava/lang/String;', + ); + + static final _toString$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String toString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toString$1() { + return _toString$1( + reference.pointer, + _id_toString$1 as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_toURI = _class.instanceMethodId( + r'toURI', + r'()Ljava/lang/String;', + ); + + static final _toURI = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String toURI()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toURI() { + return _toURI( + reference.pointer, + _id_toURI as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_toUri = _class.instanceMethodId( + r'toUri', + r'(I)Ljava/lang/String;', + ); + + static final _toUri = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public java.lang.String toUri(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toUri(int i) { + return _toUri( + reference.pointer, + _id_toUri as jni$_.JMethodIDPtr, + i, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_writeToParcel = _class.instanceMethodId( + r'writeToParcel', + r'(Landroid/os/Parcel;I)V', + ); + + static final _writeToParcel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public void writeToParcel(android.os.Parcel parcel, int i)` + void writeToParcel(jni$_.JObject? parcel, int i) { + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _writeToParcel( + reference.pointer, + _id_writeToParcel as jni$_.JMethodIDPtr, + _$parcel.pointer, + i, + ).check(); + } +} + +final class $Intent$NullableType$ extends jni$_.JType { + @jni$_.internal + const $Intent$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent;'; + + @jni$_.internal + @core$_.override + Intent? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Intent.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$NullableType$) && + other is $Intent$NullableType$; + } +} + +final class $Intent$Type$ extends jni$_.JType { + @jni$_.internal + const $Intent$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/Intent;'; + + @jni$_.internal + @core$_.override + Intent fromReference(jni$_.JReference reference) => + Intent.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Intent$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Intent$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Intent$Type$) && other is $Intent$Type$; + } +} + +/// from: `androidx.core.content.ContextCompat$RegisterReceiverFlags` +class ContextCompat$RegisterReceiverFlags extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + ContextCompat$RegisterReceiverFlags.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/core/content/ContextCompat$RegisterReceiverFlags', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $ContextCompat$RegisterReceiverFlags$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $ContextCompat$RegisterReceiverFlags$Type$(); + + /// Maps a specific port to the implemented interface. + static final core$_.Map _$impls = + {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses(0, descriptor.address, args.address), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $ContextCompat$RegisterReceiverFlags $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'androidx.core.content.ContextCompat$RegisterReceiverFlags', + $p, + _$invokePointer, + [], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory ContextCompat$RegisterReceiverFlags.implement( + $ContextCompat$RegisterReceiverFlags $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return ContextCompat$RegisterReceiverFlags.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $ContextCompat$RegisterReceiverFlags { + factory $ContextCompat$RegisterReceiverFlags() = + _$ContextCompat$RegisterReceiverFlags; +} + +final class _$ContextCompat$RegisterReceiverFlags + with $ContextCompat$RegisterReceiverFlags { + _$ContextCompat$RegisterReceiverFlags(); +} + +final class $ContextCompat$RegisterReceiverFlags$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $ContextCompat$RegisterReceiverFlags$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/core/content/ContextCompat$RegisterReceiverFlags;'; + + @jni$_.internal + @core$_.override + ContextCompat$RegisterReceiverFlags? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : ContextCompat$RegisterReceiverFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($ContextCompat$RegisterReceiverFlags$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ContextCompat$RegisterReceiverFlags$NullableType$) && + other is $ContextCompat$RegisterReceiverFlags$NullableType$; + } +} + +final class $ContextCompat$RegisterReceiverFlags$Type$ + extends jni$_.JType { + @jni$_.internal + const $ContextCompat$RegisterReceiverFlags$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroidx/core/content/ContextCompat$RegisterReceiverFlags;'; + + @jni$_.internal + @core$_.override + ContextCompat$RegisterReceiverFlags fromReference( + jni$_.JReference reference, + ) => ContextCompat$RegisterReceiverFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $ContextCompat$RegisterReceiverFlags$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ContextCompat$RegisterReceiverFlags$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ContextCompat$RegisterReceiverFlags$Type$) && + other is $ContextCompat$RegisterReceiverFlags$Type$; + } +} + +/// from: `androidx.core.content.ContextCompat` +class ContextCompat extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + ContextCompat.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'androidx/core/content/ContextCompat', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $ContextCompat$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $ContextCompat$Type$(); + + /// from: `static public final int RECEIVER_VISIBLE_TO_INSTANT_APPS` + static const RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; + + /// from: `static public final int RECEIVER_EXPORTED` + static const RECEIVER_EXPORTED = 2; + + /// from: `static public final int RECEIVER_NOT_EXPORTED` + static const RECEIVER_NOT_EXPORTED = 4; + static final _id_startActivities = _class.staticMethodId( + r'startActivities', + r'(Landroid/content/Context;[Landroid/content/Intent;)Z', + ); + + static final _startActivities = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public boolean startActivities(android.content.Context context, android.content.Intent[] intents)` + static bool startActivities( + jni$_.JObject? context, + jni$_.JArray? intents, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$intents = intents?.reference ?? jni$_.jNullReference; + return _startActivities( + _class.reference.pointer, + _id_startActivities as jni$_.JMethodIDPtr, + _$context.pointer, + _$intents.pointer, + ).boolean; + } + + static final _id_startActivities$1 = _class.staticMethodId( + r'startActivities', + r'(Landroid/content/Context;[Landroid/content/Intent;Landroid/os/Bundle;)Z', + ); + + static final _startActivities$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public boolean startActivities(android.content.Context context, android.content.Intent[] intents, android.os.Bundle bundle)` + static bool startActivities$1( + jni$_.JObject? context, + jni$_.JArray? intents, + jni$_.JObject? bundle, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$intents = intents?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + return _startActivities$1( + _class.reference.pointer, + _id_startActivities$1 as jni$_.JMethodIDPtr, + _$context.pointer, + _$intents.pointer, + _$bundle.pointer, + ).boolean; + } + + static final _id_startActivity = _class.staticMethodId( + r'startActivity', + r'(Landroid/content/Context;Landroid/content/Intent;Landroid/os/Bundle;)V', + ); + + static final _startActivity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public void startActivity(android.content.Context context, android.content.Intent intent, android.os.Bundle bundle)` + static void startActivity( + jni$_.JObject? context, + Intent? intent, + jni$_.JObject? bundle, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$bundle = bundle?.reference ?? jni$_.jNullReference; + _startActivity( + _class.reference.pointer, + _id_startActivity as jni$_.JMethodIDPtr, + _$context.pointer, + _$intent.pointer, + _$bundle.pointer, + ).check(); + } + + static final _id_getDataDir = _class.staticMethodId( + r'getDataDir', + r'(Landroid/content/Context;)Ljava/io/File;', + ); + + static final _getDataDir = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.io.File getDataDir(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getDataDir(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getDataDir( + _class.reference.pointer, + _id_getDataDir as jni$_.JMethodIDPtr, + _$context.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getObbDirs = _class.staticMethodId( + r'getObbDirs', + r'(Landroid/content/Context;)[Ljava/io/File;', + ); + + static final _getObbDirs = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.io.File[] getObbDirs(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JArray? getObbDirs(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getObbDirs( + _class.reference.pointer, + _id_getObbDirs as jni$_.JMethodIDPtr, + _$context.pointer, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getExternalFilesDirs = _class.staticMethodId( + r'getExternalFilesDirs', + r'(Landroid/content/Context;Ljava/lang/String;)[Ljava/io/File;', + ); + + static final _getExternalFilesDirs = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.io.File[] getExternalFilesDirs(android.content.Context context, java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JArray? getExternalFilesDirs( + jni$_.JObject? context, + jni$_.JString? string, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _getExternalFilesDirs( + _class.reference.pointer, + _id_getExternalFilesDirs as jni$_.JMethodIDPtr, + _$context.pointer, + _$string.pointer, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getExternalCacheDirs = _class.staticMethodId( + r'getExternalCacheDirs', + r'(Landroid/content/Context;)[Ljava/io/File;', + ); + + static final _getExternalCacheDirs = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.io.File[] getExternalCacheDirs(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JArray? getExternalCacheDirs( + jni$_.JObject? context, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getExternalCacheDirs( + _class.reference.pointer, + _id_getExternalCacheDirs as jni$_.JMethodIDPtr, + _$context.pointer, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getDrawable = _class.staticMethodId( + r'getDrawable', + r'(Landroid/content/Context;I)Landroid/graphics/drawable/Drawable;', + ); + + static final _getDrawable = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public android.graphics.drawable.Drawable getDrawable(android.content.Context context, int i)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getDrawable(jni$_.JObject? context, int i) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getDrawable( + _class.reference.pointer, + _id_getDrawable as jni$_.JMethodIDPtr, + _$context.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getColorStateList = _class.staticMethodId( + r'getColorStateList', + r'(Landroid/content/Context;I)Landroid/content/res/ColorStateList;', + ); + + static final _getColorStateList = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public android.content.res.ColorStateList getColorStateList(android.content.Context context, int i)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getColorStateList(jni$_.JObject? context, int i) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getColorStateList( + _class.reference.pointer, + _id_getColorStateList as jni$_.JMethodIDPtr, + _$context.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getColor = _class.staticMethodId( + r'getColor', + r'(Landroid/content/Context;I)I', + ); + + static final _getColor = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallStaticIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public int getColor(android.content.Context context, int i)` + static int getColor(jni$_.JObject? context, int i) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getColor( + _class.reference.pointer, + _id_getColor as jni$_.JMethodIDPtr, + _$context.pointer, + i, + ).integer; + } + + static final _id_checkSelfPermission = _class.staticMethodId( + r'checkSelfPermission', + r'(Landroid/content/Context;Ljava/lang/String;)I', + ); + + static final _checkSelfPermission = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public int checkSelfPermission(android.content.Context context, java.lang.String string)` + static int checkSelfPermission( + jni$_.JObject? context, + jni$_.JString? string, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _checkSelfPermission( + _class.reference.pointer, + _id_checkSelfPermission as jni$_.JMethodIDPtr, + _$context.pointer, + _$string.pointer, + ).integer; + } + + static final _id_getNoBackupFilesDir = _class.staticMethodId( + r'getNoBackupFilesDir', + r'(Landroid/content/Context;)Ljava/io/File;', + ); + + static final _getNoBackupFilesDir = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.io.File getNoBackupFilesDir(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getNoBackupFilesDir(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getNoBackupFilesDir( + _class.reference.pointer, + _id_getNoBackupFilesDir as jni$_.JMethodIDPtr, + _$context.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getCodeCacheDir = _class.staticMethodId( + r'getCodeCacheDir', + r'(Landroid/content/Context;)Ljava/io/File;', + ); + + static final _getCodeCacheDir = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.io.File getCodeCacheDir(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getCodeCacheDir(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getCodeCacheDir( + _class.reference.pointer, + _id_getCodeCacheDir as jni$_.JMethodIDPtr, + _$context.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_createDeviceProtectedStorageContext = _class.staticMethodId( + r'createDeviceProtectedStorageContext', + r'(Landroid/content/Context;)Landroid/content/Context;', + ); + + static final _createDeviceProtectedStorageContext = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Context createDeviceProtectedStorageContext(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? createDeviceProtectedStorageContext( + jni$_.JObject? context, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _createDeviceProtectedStorageContext( + _class.reference.pointer, + _id_createDeviceProtectedStorageContext as jni$_.JMethodIDPtr, + _$context.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_isDeviceProtectedStorage = _class.staticMethodId( + r'isDeviceProtectedStorage', + r'(Landroid/content/Context;)Z', + ); + + static final _isDeviceProtectedStorage = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public boolean isDeviceProtectedStorage(android.content.Context context)` + static bool isDeviceProtectedStorage(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _isDeviceProtectedStorage( + _class.reference.pointer, + _id_isDeviceProtectedStorage as jni$_.JMethodIDPtr, + _$context.pointer, + ).boolean; + } + + static final _id_getMainExecutor = _class.staticMethodId( + r'getMainExecutor', + r'(Landroid/content/Context;)Ljava/util/concurrent/Executor;', + ); + + static final _getMainExecutor = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.util.concurrent.Executor getMainExecutor(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getMainExecutor(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getMainExecutor( + _class.reference.pointer, + _id_getMainExecutor as jni$_.JMethodIDPtr, + _$context.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_startForegroundService = _class.staticMethodId( + r'startForegroundService', + r'(Landroid/content/Context;Landroid/content/Intent;)V', + ); + + static final _startForegroundService = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public void startForegroundService(android.content.Context context, android.content.Intent intent)` + static void startForegroundService(jni$_.JObject? context, Intent? intent) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + _startForegroundService( + _class.reference.pointer, + _id_startForegroundService as jni$_.JMethodIDPtr, + _$context.pointer, + _$intent.pointer, + ).check(); + } + + static final _id_getDisplayOrDefault = _class.staticMethodId( + r'getDisplayOrDefault', + r'(Landroid/content/Context;)Landroid/view/Display;', + ); + + static final _getDisplayOrDefault = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.view.Display getDisplayOrDefault(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getDisplayOrDefault(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getDisplayOrDefault( + _class.reference.pointer, + _id_getDisplayOrDefault as jni$_.JMethodIDPtr, + _$context.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSystemService = _class.staticMethodId( + r'getSystemService', + r'(Landroid/content/Context;Ljava/lang/Class;)Ljava/lang/Object;', + ); + + static final _getSystemService = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public T getSystemService(android.content.Context context, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + static $T? getSystemService<$T extends jni$_.JObject?>( + jni$_.JObject? context, + jni$_.JObject? class$, { + required jni$_.JType<$T> T, + }) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getSystemService( + _class.reference.pointer, + _id_getSystemService as jni$_.JMethodIDPtr, + _$context.pointer, + _$class$.pointer, + ).object<$T?>(T.nullableType); + } + + static final _id_registerReceiver = _class.staticMethodId( + r'registerReceiver', + r'(Landroid/content/Context;Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;I)Landroid/content/Intent;', + ); + + static final _registerReceiver = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public android.content.Intent registerReceiver(android.content.Context context, android.content.BroadcastReceiver broadcastReceiver, android.content.IntentFilter intentFilter, int i)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? registerReceiver( + jni$_.JObject? context, + jni$_.JObject? broadcastReceiver, + jni$_.JObject? intentFilter, + int i, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$broadcastReceiver = + broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$intentFilter = intentFilter?.reference ?? jni$_.jNullReference; + return _registerReceiver( + _class.reference.pointer, + _id_registerReceiver as jni$_.JMethodIDPtr, + _$context.pointer, + _$broadcastReceiver.pointer, + _$intentFilter.pointer, + i, + ).object(const $Intent$NullableType$()); + } + + static final _id_registerReceiver$1 = _class.staticMethodId( + r'registerReceiver', + r'(Landroid/content/Context;Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;Ljava/lang/String;Landroid/os/Handler;I)Landroid/content/Intent;', + ); + + static final _registerReceiver$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public android.content.Intent registerReceiver(android.content.Context context, android.content.BroadcastReceiver broadcastReceiver, android.content.IntentFilter intentFilter, java.lang.String string, android.os.Handler handler, int i)` + /// The returned object must be released after use, by calling the [release] method. + static Intent? registerReceiver$1( + jni$_.JObject? context, + jni$_.JObject? broadcastReceiver, + jni$_.JObject? intentFilter, + jni$_.JString? string, + jni$_.JObject? handler, + int i, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$broadcastReceiver = + broadcastReceiver?.reference ?? jni$_.jNullReference; + final _$intentFilter = intentFilter?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + final _$handler = handler?.reference ?? jni$_.jNullReference; + return _registerReceiver$1( + _class.reference.pointer, + _id_registerReceiver$1 as jni$_.JMethodIDPtr, + _$context.pointer, + _$broadcastReceiver.pointer, + _$intentFilter.pointer, + _$string.pointer, + _$handler.pointer, + i, + ).object(const $Intent$NullableType$()); + } + + static final _id_getSystemServiceName = _class.staticMethodId( + r'getSystemServiceName', + r'(Landroid/content/Context;Ljava/lang/Class;)Ljava/lang/String;', + ); + + static final _getSystemServiceName = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.lang.String getSystemServiceName(android.content.Context context, java.lang.Class class)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? getSystemServiceName( + jni$_.JObject? context, + jni$_.JObject? class$, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$class$ = class$?.reference ?? jni$_.jNullReference; + return _getSystemServiceName( + _class.reference.pointer, + _id_getSystemServiceName as jni$_.JMethodIDPtr, + _$context.pointer, + _$class$.pointer, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getString = _class.staticMethodId( + r'getString', + r'(Landroid/content/Context;I)Ljava/lang/String;', + ); + + static final _getString = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public java.lang.String getString(android.content.Context context, int i)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? getString(jni$_.JObject? context, int i) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getString( + _class.reference.pointer, + _id_getString as jni$_.JMethodIDPtr, + _$context.pointer, + i, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getContextForLanguage = _class.staticMethodId( + r'getContextForLanguage', + r'(Landroid/content/Context;)Landroid/content/Context;', + ); + + static final _getContextForLanguage = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Context getContextForLanguage(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getContextForLanguage(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getContextForLanguage( + _class.reference.pointer, + _id_getContextForLanguage as jni$_.JMethodIDPtr, + _$context.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getAttributionTag = _class.staticMethodId( + r'getAttributionTag', + r'(Landroid/content/Context;)Ljava/lang/String;', + ); + + static final _getAttributionTag = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `static public java.lang.String getAttributionTag(android.content.Context context)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? getAttributionTag(jni$_.JObject? context) { + final _$context = context?.reference ?? jni$_.jNullReference; + return _getAttributionTag( + _class.reference.pointer, + _id_getAttributionTag as jni$_.JMethodIDPtr, + _$context.pointer, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_createAttributionContext = _class.staticMethodId( + r'createAttributionContext', + r'(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Context;', + ); + + static final _createAttributionContext = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public android.content.Context createAttributionContext(android.content.Context context, java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? createAttributionContext( + jni$_.JObject? context, + jni$_.JString? string, + ) { + final _$context = context?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _createAttributionContext( + _class.reference.pointer, + _id_createAttributionContext as jni$_.JMethodIDPtr, + _$context.pointer, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } +} + +final class $ContextCompat$NullableType$ extends jni$_.JType { + @jni$_.internal + const $ContextCompat$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/core/content/ContextCompat;'; + + @jni$_.internal + @core$_.override + ContextCompat? fromReference(jni$_.JReference reference) => + reference.isNull ? null : ContextCompat.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ContextCompat$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ContextCompat$NullableType$) && + other is $ContextCompat$NullableType$; + } +} + +final class $ContextCompat$Type$ extends jni$_.JType { + @jni$_.internal + const $ContextCompat$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroidx/core/content/ContextCompat;'; + + @jni$_.internal + @core$_.override + ContextCompat fromReference(jni$_.JReference reference) => + ContextCompat.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $ContextCompat$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ContextCompat$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ContextCompat$Type$) && + other is $ContextCompat$Type$; + } +} + +/// from: `android.Manifest$permission` +class Manifest$permission extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Manifest$permission.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName(r'android/Manifest$permission'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $Manifest$permission$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $Manifest$permission$Type$(); + static final _id_ACCEPT_HANDOVER = _class.staticFieldId( + r'ACCEPT_HANDOVER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCEPT_HANDOVER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCEPT_HANDOVER => + _id_ACCEPT_HANDOVER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACCESS_BACKGROUND_LOCATION = _class.staticFieldId( + r'ACCESS_BACKGROUND_LOCATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_BACKGROUND_LOCATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_BACKGROUND_LOCATION => + _id_ACCESS_BACKGROUND_LOCATION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACCESS_BLOBS_ACROSS_USERS = _class.staticFieldId( + r'ACCESS_BLOBS_ACROSS_USERS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_BLOBS_ACROSS_USERS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_BLOBS_ACROSS_USERS => + _id_ACCESS_BLOBS_ACROSS_USERS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACCESS_CHECKIN_PROPERTIES = _class.staticFieldId( + r'ACCESS_CHECKIN_PROPERTIES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_CHECKIN_PROPERTIES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_CHECKIN_PROPERTIES => + _id_ACCESS_CHECKIN_PROPERTIES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACCESS_COARSE_LOCATION = _class.staticFieldId( + r'ACCESS_COARSE_LOCATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_COARSE_LOCATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_COARSE_LOCATION => _id_ACCESS_COARSE_LOCATION + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACCESS_FINE_LOCATION = _class.staticFieldId( + r'ACCESS_FINE_LOCATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_FINE_LOCATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_FINE_LOCATION => _id_ACCESS_FINE_LOCATION + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACCESS_HIDDEN_PROFILES = _class.staticFieldId( + r'ACCESS_HIDDEN_PROFILES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_HIDDEN_PROFILES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_HIDDEN_PROFILES => _id_ACCESS_HIDDEN_PROFILES + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACCESS_LOCATION_EXTRA_COMMANDS = _class.staticFieldId( + r'ACCESS_LOCATION_EXTRA_COMMANDS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_LOCATION_EXTRA_COMMANDS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_LOCATION_EXTRA_COMMANDS => + _id_ACCESS_LOCATION_EXTRA_COMMANDS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACCESS_MEDIA_LOCATION = _class.staticFieldId( + r'ACCESS_MEDIA_LOCATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_MEDIA_LOCATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_MEDIA_LOCATION => _id_ACCESS_MEDIA_LOCATION + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACCESS_NETWORK_STATE = _class.staticFieldId( + r'ACCESS_NETWORK_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_NETWORK_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_NETWORK_STATE => _id_ACCESS_NETWORK_STATE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACCESS_NOTIFICATION_POLICY = _class.staticFieldId( + r'ACCESS_NOTIFICATION_POLICY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_NOTIFICATION_POLICY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_NOTIFICATION_POLICY => + _id_ACCESS_NOTIFICATION_POLICY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_ACCESS_WIFI_STATE = _class.staticFieldId( + r'ACCESS_WIFI_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCESS_WIFI_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCESS_WIFI_STATE => + _id_ACCESS_WIFI_STATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACCOUNT_MANAGER = _class.staticFieldId( + r'ACCOUNT_MANAGER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACCOUNT_MANAGER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACCOUNT_MANAGER => + _id_ACCOUNT_MANAGER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ACTIVITY_RECOGNITION = _class.staticFieldId( + r'ACTIVITY_RECOGNITION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTIVITY_RECOGNITION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTIVITY_RECOGNITION => _id_ACTIVITY_RECOGNITION + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ADD_VOICEMAIL = _class.staticFieldId( + r'ADD_VOICEMAIL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ADD_VOICEMAIL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ADD_VOICEMAIL => + _id_ADD_VOICEMAIL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ANSWER_PHONE_CALLS = _class.staticFieldId( + r'ANSWER_PHONE_CALLS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ANSWER_PHONE_CALLS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ANSWER_PHONE_CALLS => + _id_ANSWER_PHONE_CALLS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_APPLY_PICTURE_PROFILE = _class.staticFieldId( + r'APPLY_PICTURE_PROFILE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String APPLY_PICTURE_PROFILE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get APPLY_PICTURE_PROFILE => _id_APPLY_PICTURE_PROFILE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BATTERY_STATS = _class.staticFieldId( + r'BATTERY_STATS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BATTERY_STATS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BATTERY_STATS => + _id_BATTERY_STATS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_ACCESSIBILITY_SERVICE = _class.staticFieldId( + r'BIND_ACCESSIBILITY_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_ACCESSIBILITY_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_ACCESSIBILITY_SERVICE => + _id_BIND_ACCESSIBILITY_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_APPWIDGET = _class.staticFieldId( + r'BIND_APPWIDGET', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_APPWIDGET` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_APPWIDGET => + _id_BIND_APPWIDGET.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_APP_FUNCTION_SERVICE = _class.staticFieldId( + r'BIND_APP_FUNCTION_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_APP_FUNCTION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_APP_FUNCTION_SERVICE => + _id_BIND_APP_FUNCTION_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_AUTOFILL_SERVICE = _class.staticFieldId( + r'BIND_AUTOFILL_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_AUTOFILL_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_AUTOFILL_SERVICE => _id_BIND_AUTOFILL_SERVICE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_CALL_REDIRECTION_SERVICE = _class.staticFieldId( + r'BIND_CALL_REDIRECTION_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_CALL_REDIRECTION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_CALL_REDIRECTION_SERVICE => + _id_BIND_CALL_REDIRECTION_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_CARRIER_MESSAGING_CLIENT_SERVICE = _class.staticFieldId( + r'BIND_CARRIER_MESSAGING_CLIENT_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_CARRIER_MESSAGING_CLIENT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_CARRIER_MESSAGING_CLIENT_SERVICE => + _id_BIND_CARRIER_MESSAGING_CLIENT_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_CARRIER_MESSAGING_SERVICE = _class.staticFieldId( + r'BIND_CARRIER_MESSAGING_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_CARRIER_MESSAGING_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_CARRIER_MESSAGING_SERVICE => + _id_BIND_CARRIER_MESSAGING_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_CARRIER_SERVICES = _class.staticFieldId( + r'BIND_CARRIER_SERVICES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_CARRIER_SERVICES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_CARRIER_SERVICES => _id_BIND_CARRIER_SERVICES + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_CHOOSER_TARGET_SERVICE = _class.staticFieldId( + r'BIND_CHOOSER_TARGET_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_CHOOSER_TARGET_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_CHOOSER_TARGET_SERVICE => + _id_BIND_CHOOSER_TARGET_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_COMPANION_DEVICE_SERVICE = _class.staticFieldId( + r'BIND_COMPANION_DEVICE_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_COMPANION_DEVICE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_COMPANION_DEVICE_SERVICE => + _id_BIND_COMPANION_DEVICE_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_CONDITION_PROVIDER_SERVICE = _class.staticFieldId( + r'BIND_CONDITION_PROVIDER_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_CONDITION_PROVIDER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_CONDITION_PROVIDER_SERVICE => + _id_BIND_CONDITION_PROVIDER_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_CONTROLS = _class.staticFieldId( + r'BIND_CONTROLS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_CONTROLS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_CONTROLS => + _id_BIND_CONTROLS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_CREDENTIAL_PROVIDER_SERVICE = _class.staticFieldId( + r'BIND_CREDENTIAL_PROVIDER_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_CREDENTIAL_PROVIDER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_CREDENTIAL_PROVIDER_SERVICE => + _id_BIND_CREDENTIAL_PROVIDER_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_DEVICE_ADMIN = _class.staticFieldId( + r'BIND_DEVICE_ADMIN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_DEVICE_ADMIN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_DEVICE_ADMIN => + _id_BIND_DEVICE_ADMIN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_DREAM_SERVICE = _class.staticFieldId( + r'BIND_DREAM_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_DREAM_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_DREAM_SERVICE => + _id_BIND_DREAM_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_INCALL_SERVICE = _class.staticFieldId( + r'BIND_INCALL_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_INCALL_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_INCALL_SERVICE => + _id_BIND_INCALL_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_INPUT_METHOD = _class.staticFieldId( + r'BIND_INPUT_METHOD', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_INPUT_METHOD` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_INPUT_METHOD => + _id_BIND_INPUT_METHOD.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_MIDI_DEVICE_SERVICE = _class.staticFieldId( + r'BIND_MIDI_DEVICE_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_MIDI_DEVICE_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_MIDI_DEVICE_SERVICE => + _id_BIND_MIDI_DEVICE_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_NFC_SERVICE = _class.staticFieldId( + r'BIND_NFC_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_NFC_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_NFC_SERVICE => + _id_BIND_NFC_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_NOTIFICATION_LISTENER_SERVICE = _class.staticFieldId( + r'BIND_NOTIFICATION_LISTENER_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_NOTIFICATION_LISTENER_SERVICE => + _id_BIND_NOTIFICATION_LISTENER_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_PRINT_SERVICE = _class.staticFieldId( + r'BIND_PRINT_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_PRINT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_PRINT_SERVICE => + _id_BIND_PRINT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_QUICK_ACCESS_WALLET_SERVICE = _class.staticFieldId( + r'BIND_QUICK_ACCESS_WALLET_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_QUICK_ACCESS_WALLET_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_QUICK_ACCESS_WALLET_SERVICE => + _id_BIND_QUICK_ACCESS_WALLET_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_QUICK_SETTINGS_TILE = _class.staticFieldId( + r'BIND_QUICK_SETTINGS_TILE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_QUICK_SETTINGS_TILE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_QUICK_SETTINGS_TILE => + _id_BIND_QUICK_SETTINGS_TILE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_REMOTEVIEWS = _class.staticFieldId( + r'BIND_REMOTEVIEWS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_REMOTEVIEWS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_REMOTEVIEWS => + _id_BIND_REMOTEVIEWS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_SCREENING_SERVICE = _class.staticFieldId( + r'BIND_SCREENING_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_SCREENING_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_SCREENING_SERVICE => _id_BIND_SCREENING_SERVICE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_TELECOM_CONNECTION_SERVICE = _class.staticFieldId( + r'BIND_TELECOM_CONNECTION_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_TELECOM_CONNECTION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_TELECOM_CONNECTION_SERVICE => + _id_BIND_TELECOM_CONNECTION_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_TEXT_SERVICE = _class.staticFieldId( + r'BIND_TEXT_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_TEXT_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_TEXT_SERVICE => + _id_BIND_TEXT_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_TV_AD_SERVICE = _class.staticFieldId( + r'BIND_TV_AD_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_TV_AD_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_TV_AD_SERVICE => + _id_BIND_TV_AD_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_TV_INPUT = _class.staticFieldId( + r'BIND_TV_INPUT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_TV_INPUT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_TV_INPUT => + _id_BIND_TV_INPUT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_TV_INTERACTIVE_APP = _class.staticFieldId( + r'BIND_TV_INTERACTIVE_APP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_TV_INTERACTIVE_APP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_TV_INTERACTIVE_APP => + _id_BIND_TV_INTERACTIVE_APP.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_VISUAL_VOICEMAIL_SERVICE = _class.staticFieldId( + r'BIND_VISUAL_VOICEMAIL_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_VISUAL_VOICEMAIL_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_VISUAL_VOICEMAIL_SERVICE => + _id_BIND_VISUAL_VOICEMAIL_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_VOICE_INTERACTION = _class.staticFieldId( + r'BIND_VOICE_INTERACTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_VOICE_INTERACTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_VOICE_INTERACTION => _id_BIND_VOICE_INTERACTION + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_VPN_SERVICE = _class.staticFieldId( + r'BIND_VPN_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_VPN_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_VPN_SERVICE => + _id_BIND_VPN_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BIND_VR_LISTENER_SERVICE = _class.staticFieldId( + r'BIND_VR_LISTENER_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_VR_LISTENER_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_VR_LISTENER_SERVICE => + _id_BIND_VR_LISTENER_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BIND_WALLPAPER = _class.staticFieldId( + r'BIND_WALLPAPER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BIND_WALLPAPER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BIND_WALLPAPER => + _id_BIND_WALLPAPER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BLUETOOTH = _class.staticFieldId( + r'BLUETOOTH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BLUETOOTH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BLUETOOTH => + _id_BLUETOOTH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BLUETOOTH_ADMIN = _class.staticFieldId( + r'BLUETOOTH_ADMIN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BLUETOOTH_ADMIN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BLUETOOTH_ADMIN => + _id_BLUETOOTH_ADMIN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BLUETOOTH_ADVERTISE = _class.staticFieldId( + r'BLUETOOTH_ADVERTISE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BLUETOOTH_ADVERTISE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BLUETOOTH_ADVERTISE => + _id_BLUETOOTH_ADVERTISE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BLUETOOTH_CONNECT = _class.staticFieldId( + r'BLUETOOTH_CONNECT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BLUETOOTH_CONNECT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BLUETOOTH_CONNECT => + _id_BLUETOOTH_CONNECT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BLUETOOTH_PRIVILEGED = _class.staticFieldId( + r'BLUETOOTH_PRIVILEGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BLUETOOTH_PRIVILEGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BLUETOOTH_PRIVILEGED => _id_BLUETOOTH_PRIVILEGED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BLUETOOTH_SCAN = _class.staticFieldId( + r'BLUETOOTH_SCAN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BLUETOOTH_SCAN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BLUETOOTH_SCAN => + _id_BLUETOOTH_SCAN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BODY_SENSORS = _class.staticFieldId( + r'BODY_SENSORS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BODY_SENSORS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BODY_SENSORS => + _id_BODY_SENSORS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BODY_SENSORS_BACKGROUND = _class.staticFieldId( + r'BODY_SENSORS_BACKGROUND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BODY_SENSORS_BACKGROUND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BODY_SENSORS_BACKGROUND => + _id_BODY_SENSORS_BACKGROUND.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BROADCAST_PACKAGE_REMOVED = _class.staticFieldId( + r'BROADCAST_PACKAGE_REMOVED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BROADCAST_PACKAGE_REMOVED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BROADCAST_PACKAGE_REMOVED => + _id_BROADCAST_PACKAGE_REMOVED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_BROADCAST_SMS = _class.staticFieldId( + r'BROADCAST_SMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BROADCAST_SMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BROADCAST_SMS => + _id_BROADCAST_SMS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BROADCAST_STICKY = _class.staticFieldId( + r'BROADCAST_STICKY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BROADCAST_STICKY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BROADCAST_STICKY => + _id_BROADCAST_STICKY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_BROADCAST_WAP_PUSH = _class.staticFieldId( + r'BROADCAST_WAP_PUSH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String BROADCAST_WAP_PUSH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get BROADCAST_WAP_PUSH => + _id_BROADCAST_WAP_PUSH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CALL_COMPANION_APP = _class.staticFieldId( + r'CALL_COMPANION_APP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CALL_COMPANION_APP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CALL_COMPANION_APP => + _id_CALL_COMPANION_APP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CALL_PHONE = _class.staticFieldId( + r'CALL_PHONE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CALL_PHONE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CALL_PHONE => + _id_CALL_PHONE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CALL_PRIVILEGED = _class.staticFieldId( + r'CALL_PRIVILEGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CALL_PRIVILEGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CALL_PRIVILEGED => + _id_CALL_PRIVILEGED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CAMERA = _class.staticFieldId( + r'CAMERA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CAMERA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CAMERA => + _id_CAMERA.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CAPTURE_AUDIO_OUTPUT = _class.staticFieldId( + r'CAPTURE_AUDIO_OUTPUT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CAPTURE_AUDIO_OUTPUT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CAPTURE_AUDIO_OUTPUT => _id_CAPTURE_AUDIO_OUTPUT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CHANGE_COMPONENT_ENABLED_STATE = _class.staticFieldId( + r'CHANGE_COMPONENT_ENABLED_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CHANGE_COMPONENT_ENABLED_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CHANGE_COMPONENT_ENABLED_STATE => + _id_CHANGE_COMPONENT_ENABLED_STATE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CHANGE_CONFIGURATION = _class.staticFieldId( + r'CHANGE_CONFIGURATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CHANGE_CONFIGURATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CHANGE_CONFIGURATION => _id_CHANGE_CONFIGURATION + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CHANGE_NETWORK_STATE = _class.staticFieldId( + r'CHANGE_NETWORK_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CHANGE_NETWORK_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CHANGE_NETWORK_STATE => _id_CHANGE_NETWORK_STATE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CHANGE_WIFI_MULTICAST_STATE = _class.staticFieldId( + r'CHANGE_WIFI_MULTICAST_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CHANGE_WIFI_MULTICAST_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CHANGE_WIFI_MULTICAST_STATE => + _id_CHANGE_WIFI_MULTICAST_STATE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CHANGE_WIFI_STATE = _class.staticFieldId( + r'CHANGE_WIFI_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CHANGE_WIFI_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CHANGE_WIFI_STATE => + _id_CHANGE_WIFI_STATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CLEAR_APP_CACHE = _class.staticFieldId( + r'CLEAR_APP_CACHE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CLEAR_APP_CACHE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CLEAR_APP_CACHE => + _id_CLEAR_APP_CACHE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CONFIGURE_WIFI_DISPLAY = _class.staticFieldId( + r'CONFIGURE_WIFI_DISPLAY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CONFIGURE_WIFI_DISPLAY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CONFIGURE_WIFI_DISPLAY => _id_CONFIGURE_WIFI_DISPLAY + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CONTROL_LOCATION_UPDATES = _class.staticFieldId( + r'CONTROL_LOCATION_UPDATES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CONTROL_LOCATION_UPDATES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CONTROL_LOCATION_UPDATES => + _id_CONTROL_LOCATION_UPDATES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS = _class + .staticFieldId( + r'CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS => + _id_CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS = _class + .staticFieldId( + r'CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS => + _id_CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_CREDENTIAL_MANAGER_SET_ORIGIN = _class.staticFieldId( + r'CREDENTIAL_MANAGER_SET_ORIGIN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CREDENTIAL_MANAGER_SET_ORIGIN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CREDENTIAL_MANAGER_SET_ORIGIN => + _id_CREDENTIAL_MANAGER_SET_ORIGIN.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_DELETE_CACHE_FILES = _class.staticFieldId( + r'DELETE_CACHE_FILES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String DELETE_CACHE_FILES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get DELETE_CACHE_FILES => + _id_DELETE_CACHE_FILES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DELETE_PACKAGES = _class.staticFieldId( + r'DELETE_PACKAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String DELETE_PACKAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get DELETE_PACKAGES => + _id_DELETE_PACKAGES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DELIVER_COMPANION_MESSAGES = _class.staticFieldId( + r'DELIVER_COMPANION_MESSAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String DELIVER_COMPANION_MESSAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get DELIVER_COMPANION_MESSAGES => + _id_DELIVER_COMPANION_MESSAGES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_DETECT_SCREEN_CAPTURE = _class.staticFieldId( + r'DETECT_SCREEN_CAPTURE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String DETECT_SCREEN_CAPTURE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get DETECT_SCREEN_CAPTURE => _id_DETECT_SCREEN_CAPTURE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DETECT_SCREEN_RECORDING = _class.staticFieldId( + r'DETECT_SCREEN_RECORDING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String DETECT_SCREEN_RECORDING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get DETECT_SCREEN_RECORDING => + _id_DETECT_SCREEN_RECORDING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_DIAGNOSTIC = _class.staticFieldId( + r'DIAGNOSTIC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String DIAGNOSTIC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get DIAGNOSTIC => + _id_DIAGNOSTIC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DISABLE_KEYGUARD = _class.staticFieldId( + r'DISABLE_KEYGUARD', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String DISABLE_KEYGUARD` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get DISABLE_KEYGUARD => + _id_DISABLE_KEYGUARD.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_DUMP = _class.staticFieldId(r'DUMP', r'Ljava/lang/String;'); + + /// from: `static public final java.lang.String DUMP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get DUMP => + _id_DUMP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_ENFORCE_UPDATE_OWNERSHIP = _class.staticFieldId( + r'ENFORCE_UPDATE_OWNERSHIP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ENFORCE_UPDATE_OWNERSHIP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ENFORCE_UPDATE_OWNERSHIP => + _id_ENFORCE_UPDATE_OWNERSHIP.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_EXECUTE_APP_ACTION = _class.staticFieldId( + r'EXECUTE_APP_ACTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXECUTE_APP_ACTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXECUTE_APP_ACTION => + _id_EXECUTE_APP_ACTION.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXECUTE_APP_FUNCTIONS = _class.staticFieldId( + r'EXECUTE_APP_FUNCTIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXECUTE_APP_FUNCTIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXECUTE_APP_FUNCTIONS => _id_EXECUTE_APP_FUNCTIONS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXPAND_STATUS_BAR = _class.staticFieldId( + r'EXPAND_STATUS_BAR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXPAND_STATUS_BAR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXPAND_STATUS_BAR => + _id_EXPAND_STATUS_BAR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FACTORY_TEST = _class.staticFieldId( + r'FACTORY_TEST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FACTORY_TEST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FACTORY_TEST => + _id_FACTORY_TEST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FOREGROUND_SERVICE = _class.staticFieldId( + r'FOREGROUND_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE => + _id_FOREGROUND_SERVICE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FOREGROUND_SERVICE_CAMERA = _class.staticFieldId( + r'FOREGROUND_SERVICE_CAMERA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_CAMERA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_CAMERA => + _id_FOREGROUND_SERVICE_CAMERA.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_CONNECTED_DEVICE = _class.staticFieldId( + r'FOREGROUND_SERVICE_CONNECTED_DEVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_CONNECTED_DEVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_CONNECTED_DEVICE => + _id_FOREGROUND_SERVICE_CONNECTED_DEVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_DATA_SYNC = _class.staticFieldId( + r'FOREGROUND_SERVICE_DATA_SYNC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_DATA_SYNC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_DATA_SYNC => + _id_FOREGROUND_SERVICE_DATA_SYNC.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_HEALTH = _class.staticFieldId( + r'FOREGROUND_SERVICE_HEALTH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_HEALTH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_HEALTH => + _id_FOREGROUND_SERVICE_HEALTH.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_LOCATION = _class.staticFieldId( + r'FOREGROUND_SERVICE_LOCATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_LOCATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_LOCATION => + _id_FOREGROUND_SERVICE_LOCATION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_MEDIA_PLAYBACK = _class.staticFieldId( + r'FOREGROUND_SERVICE_MEDIA_PLAYBACK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_MEDIA_PLAYBACK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_MEDIA_PLAYBACK => + _id_FOREGROUND_SERVICE_MEDIA_PLAYBACK.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_MEDIA_PROCESSING = _class.staticFieldId( + r'FOREGROUND_SERVICE_MEDIA_PROCESSING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_MEDIA_PROCESSING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_MEDIA_PROCESSING => + _id_FOREGROUND_SERVICE_MEDIA_PROCESSING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_MEDIA_PROJECTION = _class.staticFieldId( + r'FOREGROUND_SERVICE_MEDIA_PROJECTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_MEDIA_PROJECTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_MEDIA_PROJECTION => + _id_FOREGROUND_SERVICE_MEDIA_PROJECTION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_MICROPHONE = _class.staticFieldId( + r'FOREGROUND_SERVICE_MICROPHONE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_MICROPHONE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_MICROPHONE => + _id_FOREGROUND_SERVICE_MICROPHONE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_PHONE_CALL = _class.staticFieldId( + r'FOREGROUND_SERVICE_PHONE_CALL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_PHONE_CALL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_PHONE_CALL => + _id_FOREGROUND_SERVICE_PHONE_CALL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_REMOTE_MESSAGING = _class.staticFieldId( + r'FOREGROUND_SERVICE_REMOTE_MESSAGING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_REMOTE_MESSAGING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_REMOTE_MESSAGING => + _id_FOREGROUND_SERVICE_REMOTE_MESSAGING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_SPECIAL_USE = _class.staticFieldId( + r'FOREGROUND_SERVICE_SPECIAL_USE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_SPECIAL_USE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_SPECIAL_USE => + _id_FOREGROUND_SERVICE_SPECIAL_USE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FOREGROUND_SERVICE_SYSTEM_EXEMPTED = _class.staticFieldId( + r'FOREGROUND_SERVICE_SYSTEM_EXEMPTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FOREGROUND_SERVICE_SYSTEM_EXEMPTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FOREGROUND_SERVICE_SYSTEM_EXEMPTED => + _id_FOREGROUND_SERVICE_SYSTEM_EXEMPTED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_GET_ACCOUNTS = _class.staticFieldId( + r'GET_ACCOUNTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String GET_ACCOUNTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get GET_ACCOUNTS => + _id_GET_ACCOUNTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_GET_ACCOUNTS_PRIVILEGED = _class.staticFieldId( + r'GET_ACCOUNTS_PRIVILEGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String GET_ACCOUNTS_PRIVILEGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get GET_ACCOUNTS_PRIVILEGED => + _id_GET_ACCOUNTS_PRIVILEGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_GET_PACKAGE_SIZE = _class.staticFieldId( + r'GET_PACKAGE_SIZE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String GET_PACKAGE_SIZE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get GET_PACKAGE_SIZE => + _id_GET_PACKAGE_SIZE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_GET_TASKS = _class.staticFieldId( + r'GET_TASKS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String GET_TASKS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get GET_TASKS => + _id_GET_TASKS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_GLOBAL_SEARCH = _class.staticFieldId( + r'GLOBAL_SEARCH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String GLOBAL_SEARCH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get GLOBAL_SEARCH => + _id_GLOBAL_SEARCH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_HIDE_OVERLAY_WINDOWS = _class.staticFieldId( + r'HIDE_OVERLAY_WINDOWS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String HIDE_OVERLAY_WINDOWS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get HIDE_OVERLAY_WINDOWS => _id_HIDE_OVERLAY_WINDOWS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_HIGH_SAMPLING_RATE_SENSORS = _class.staticFieldId( + r'HIGH_SAMPLING_RATE_SENSORS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String HIGH_SAMPLING_RATE_SENSORS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get HIGH_SAMPLING_RATE_SENSORS => + _id_HIGH_SAMPLING_RATE_SENSORS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_INSTALL_LOCATION_PROVIDER = _class.staticFieldId( + r'INSTALL_LOCATION_PROVIDER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String INSTALL_LOCATION_PROVIDER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get INSTALL_LOCATION_PROVIDER => + _id_INSTALL_LOCATION_PROVIDER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_INSTALL_PACKAGES = _class.staticFieldId( + r'INSTALL_PACKAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String INSTALL_PACKAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get INSTALL_PACKAGES => + _id_INSTALL_PACKAGES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_INSTALL_SHORTCUT = _class.staticFieldId( + r'INSTALL_SHORTCUT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String INSTALL_SHORTCUT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get INSTALL_SHORTCUT => + _id_INSTALL_SHORTCUT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_INSTANT_APP_FOREGROUND_SERVICE = _class.staticFieldId( + r'INSTANT_APP_FOREGROUND_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String INSTANT_APP_FOREGROUND_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get INSTANT_APP_FOREGROUND_SERVICE => + _id_INSTANT_APP_FOREGROUND_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_INTERACT_ACROSS_PROFILES = _class.staticFieldId( + r'INTERACT_ACROSS_PROFILES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String INTERACT_ACROSS_PROFILES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get INTERACT_ACROSS_PROFILES => + _id_INTERACT_ACROSS_PROFILES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_INTERNET = _class.staticFieldId( + r'INTERNET', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String INTERNET` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get INTERNET => + _id_INTERNET.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_KILL_BACKGROUND_PROCESSES = _class.staticFieldId( + r'KILL_BACKGROUND_PROCESSES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String KILL_BACKGROUND_PROCESSES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get KILL_BACKGROUND_PROCESSES => + _id_KILL_BACKGROUND_PROCESSES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE = _class + .staticFieldId( + r'LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE => + _id_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = _class.staticFieldId( + r'LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK => + _id_LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_LOADER_USAGE_STATS = _class.staticFieldId( + r'LOADER_USAGE_STATS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String LOADER_USAGE_STATS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get LOADER_USAGE_STATS => + _id_LOADER_USAGE_STATS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_LOCATION_HARDWARE = _class.staticFieldId( + r'LOCATION_HARDWARE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String LOCATION_HARDWARE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get LOCATION_HARDWARE => + _id_LOCATION_HARDWARE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MANAGE_DEVICE_LOCK_STATE = _class.staticFieldId( + r'MANAGE_DEVICE_LOCK_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_LOCK_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_LOCK_STATE => + _id_MANAGE_DEVICE_LOCK_STATE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_ACCESSIBILITY = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_ACCESSIBILITY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_ACCESSIBILITY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_ACCESSIBILITY => + _id_MANAGE_DEVICE_POLICY_ACCESSIBILITY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT => + _id_MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_ACROSS_USERS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_ACROSS_USERS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_ACROSS_USERS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_ACROSS_USERS => + _id_MANAGE_DEVICE_POLICY_ACROSS_USERS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL => + _id_MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? + get MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL => + _id_MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_AIRPLANE_MODE = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_AIRPLANE_MODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_AIRPLANE_MODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_AIRPLANE_MODE => + _id_MANAGE_DEVICE_POLICY_AIRPLANE_MODE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_APPS_CONTROL = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_APPS_CONTROL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_APPS_CONTROL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_APPS_CONTROL => + _id_MANAGE_DEVICE_POLICY_APPS_CONTROL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_APP_FUNCTIONS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_APP_FUNCTIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_APP_FUNCTIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_APP_FUNCTIONS => + _id_MANAGE_DEVICE_POLICY_APP_FUNCTIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_APP_RESTRICTIONS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_APP_RESTRICTIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_APP_RESTRICTIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_APP_RESTRICTIONS => + _id_MANAGE_DEVICE_POLICY_APP_RESTRICTIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_APP_USER_DATA = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_APP_USER_DATA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_APP_USER_DATA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_APP_USER_DATA => + _id_MANAGE_DEVICE_POLICY_APP_USER_DATA.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_ASSIST_CONTENT = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_ASSIST_CONTENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_ASSIST_CONTENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_ASSIST_CONTENT => + _id_MANAGE_DEVICE_POLICY_ASSIST_CONTENT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_AUDIO_OUTPUT = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_AUDIO_OUTPUT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_AUDIO_OUTPUT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_AUDIO_OUTPUT => + _id_MANAGE_DEVICE_POLICY_AUDIO_OUTPUT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_AUTOFILL = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_AUTOFILL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_AUTOFILL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_AUTOFILL => + _id_MANAGE_DEVICE_POLICY_AUTOFILL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_BACKUP_SERVICE = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_BACKUP_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_BACKUP_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_BACKUP_SERVICE => + _id_MANAGE_DEVICE_POLICY_BACKUP_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL => + _id_MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_BLUETOOTH = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_BLUETOOTH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_BLUETOOTH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_BLUETOOTH => + _id_MANAGE_DEVICE_POLICY_BLUETOOTH.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_BUGREPORT = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_BUGREPORT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_BUGREPORT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_BUGREPORT => + _id_MANAGE_DEVICE_POLICY_BUGREPORT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_CALLS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_CALLS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_CALLS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_CALLS => + _id_MANAGE_DEVICE_POLICY_CALLS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_CAMERA = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_CAMERA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_CAMERA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_CAMERA => + _id_MANAGE_DEVICE_POLICY_CAMERA.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_CAMERA_TOGGLE = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_CAMERA_TOGGLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_CAMERA_TOGGLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_CAMERA_TOGGLE => + _id_MANAGE_DEVICE_POLICY_CAMERA_TOGGLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_CERTIFICATES = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_CERTIFICATES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_CERTIFICATES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_CERTIFICATES => + _id_MANAGE_DEVICE_POLICY_CERTIFICATES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE => + _id_MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_CONTENT_PROTECTION = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_CONTENT_PROTECTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_CONTENT_PROTECTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_CONTENT_PROTECTION => + _id_MANAGE_DEVICE_POLICY_CONTENT_PROTECTION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES => + _id_MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_DEFAULT_SMS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_DEFAULT_SMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_DEFAULT_SMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_DEFAULT_SMS => + _id_MANAGE_DEVICE_POLICY_DEFAULT_SMS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS => + _id_MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_DISPLAY = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_DISPLAY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_DISPLAY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_DISPLAY => + _id_MANAGE_DEVICE_POLICY_DISPLAY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_FACTORY_RESET = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_FACTORY_RESET', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_FACTORY_RESET` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_FACTORY_RESET => + _id_MANAGE_DEVICE_POLICY_FACTORY_RESET.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_FUN = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_FUN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_FUN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_FUN => + _id_MANAGE_DEVICE_POLICY_FUN.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_INPUT_METHODS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_INPUT_METHODS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_INPUT_METHODS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_INPUT_METHODS => + _id_MANAGE_DEVICE_POLICY_INPUT_METHODS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES => + _id_MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_KEEP_UNINSTALLED_PACKAGES = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_KEEP_UNINSTALLED_PACKAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_KEEP_UNINSTALLED_PACKAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_KEEP_UNINSTALLED_PACKAGES => + _id_MANAGE_DEVICE_POLICY_KEEP_UNINSTALLED_PACKAGES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_KEYGUARD = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_KEYGUARD', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_KEYGUARD` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_KEYGUARD => + _id_MANAGE_DEVICE_POLICY_KEYGUARD.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_LOCALE = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_LOCALE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_LOCALE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_LOCALE => + _id_MANAGE_DEVICE_POLICY_LOCALE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_LOCATION = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_LOCATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_LOCATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_LOCATION => + _id_MANAGE_DEVICE_POLICY_LOCATION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_LOCK = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_LOCK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_LOCK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_LOCK => + _id_MANAGE_DEVICE_POLICY_LOCK.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS => + _id_MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_LOCK_TASK = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_LOCK_TASK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_LOCK_TASK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_LOCK_TASK => + _id_MANAGE_DEVICE_POLICY_LOCK_TASK.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS => + _id_MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_METERED_DATA = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_METERED_DATA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_METERED_DATA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_METERED_DATA => + _id_MANAGE_DEVICE_POLICY_METERED_DATA.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_MICROPHONE = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_MICROPHONE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_MICROPHONE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_MICROPHONE => + _id_MANAGE_DEVICE_POLICY_MICROPHONE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE => + _id_MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_MOBILE_NETWORK = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_MOBILE_NETWORK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_MOBILE_NETWORK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_MOBILE_NETWORK => + _id_MANAGE_DEVICE_POLICY_MOBILE_NETWORK.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_MODIFY_USERS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_MODIFY_USERS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_MODIFY_USERS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_MODIFY_USERS => + _id_MANAGE_DEVICE_POLICY_MODIFY_USERS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_MTE = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_MTE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_MTE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_MTE => + _id_MANAGE_DEVICE_POLICY_MTE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION => + _id_MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_NETWORK_LOGGING = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_NETWORK_LOGGING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_NETWORK_LOGGING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_NETWORK_LOGGING => + _id_MANAGE_DEVICE_POLICY_NETWORK_LOGGING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY => + _id_MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_OVERRIDE_APN = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_OVERRIDE_APN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_OVERRIDE_APN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_OVERRIDE_APN => + _id_MANAGE_DEVICE_POLICY_OVERRIDE_APN.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_PACKAGE_STATE = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_PACKAGE_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_PACKAGE_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_PACKAGE_STATE => + _id_MANAGE_DEVICE_POLICY_PACKAGE_STATE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_PHYSICAL_MEDIA = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_PHYSICAL_MEDIA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_PHYSICAL_MEDIA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_PHYSICAL_MEDIA => + _id_MANAGE_DEVICE_POLICY_PHYSICAL_MEDIA.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_PRINTING = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_PRINTING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_PRINTING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_PRINTING => + _id_MANAGE_DEVICE_POLICY_PRINTING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_PRIVATE_DNS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_PRIVATE_DNS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_PRIVATE_DNS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_PRIVATE_DNS => + _id_MANAGE_DEVICE_POLICY_PRIVATE_DNS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_PROFILES = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_PROFILES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_PROFILES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_PROFILES => + _id_MANAGE_DEVICE_POLICY_PROFILES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_PROFILE_INTERACTION = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_PROFILE_INTERACTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_PROFILE_INTERACTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_PROFILE_INTERACTION => + _id_MANAGE_DEVICE_POLICY_PROFILE_INTERACTION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_PROXY = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_PROXY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_PROXY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_PROXY => + _id_MANAGE_DEVICE_POLICY_PROXY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_QUERY_SYSTEM_UPDATES = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_QUERY_SYSTEM_UPDATES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_QUERY_SYSTEM_UPDATES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_QUERY_SYSTEM_UPDATES => + _id_MANAGE_DEVICE_POLICY_QUERY_SYSTEM_UPDATES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_RESET_PASSWORD = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_RESET_PASSWORD', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_RESET_PASSWORD` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_RESET_PASSWORD => + _id_MANAGE_DEVICE_POLICY_RESET_PASSWORD.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_RESTRICT_PRIVATE_DNS = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_RESTRICT_PRIVATE_DNS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_RESTRICT_PRIVATE_DNS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_RESTRICT_PRIVATE_DNS => + _id_MANAGE_DEVICE_POLICY_RESTRICT_PRIVATE_DNS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS => + _id_MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_RUN_IN_BACKGROUND = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_RUN_IN_BACKGROUND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_RUN_IN_BACKGROUND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_RUN_IN_BACKGROUND => + _id_MANAGE_DEVICE_POLICY_RUN_IN_BACKGROUND.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SAFE_BOOT = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SAFE_BOOT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SAFE_BOOT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SAFE_BOOT => + _id_MANAGE_DEVICE_POLICY_SAFE_BOOT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SCREEN_CAPTURE = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SCREEN_CAPTURE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SCREEN_CAPTURE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SCREEN_CAPTURE => + _id_MANAGE_DEVICE_POLICY_SCREEN_CAPTURE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SCREEN_CONTENT = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SCREEN_CONTENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SCREEN_CONTENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SCREEN_CONTENT => + _id_MANAGE_DEVICE_POLICY_SCREEN_CONTENT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SECURITY_LOGGING = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SECURITY_LOGGING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SECURITY_LOGGING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SECURITY_LOGGING => + _id_MANAGE_DEVICE_POLICY_SECURITY_LOGGING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SETTINGS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SETTINGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SETTINGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SETTINGS => + _id_MANAGE_DEVICE_POLICY_SETTINGS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SMS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SMS => + _id_MANAGE_DEVICE_POLICY_SMS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_STATUS_BAR = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_STATUS_BAR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_STATUS_BAR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_STATUS_BAR => + _id_MANAGE_DEVICE_POLICY_STATUS_BAR.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE => + _id_MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SUSPEND_PERSONAL_APPS = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_SUSPEND_PERSONAL_APPS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SUSPEND_PERSONAL_APPS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SUSPEND_PERSONAL_APPS => + _id_MANAGE_DEVICE_POLICY_SUSPEND_PERSONAL_APPS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SYSTEM_APPS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SYSTEM_APPS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SYSTEM_APPS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SYSTEM_APPS => + _id_MANAGE_DEVICE_POLICY_SYSTEM_APPS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS => + _id_MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_SYSTEM_UPDATES = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_SYSTEM_UPDATES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_SYSTEM_UPDATES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_SYSTEM_UPDATES => + _id_MANAGE_DEVICE_POLICY_SYSTEM_UPDATES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_THREAD_NETWORK = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_THREAD_NETWORK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_THREAD_NETWORK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_THREAD_NETWORK => + _id_MANAGE_DEVICE_POLICY_THREAD_NETWORK.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_TIME = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_TIME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_TIME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_TIME => + _id_MANAGE_DEVICE_POLICY_TIME.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING => + _id_MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER = _class + .staticFieldId( + r'MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER => + _id_MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_USERS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_USERS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_USERS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_USERS => + _id_MANAGE_DEVICE_POLICY_USERS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_VPN = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_VPN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_VPN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_VPN => + _id_MANAGE_DEVICE_POLICY_VPN.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_WALLPAPER = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_WALLPAPER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_WALLPAPER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_WALLPAPER => + _id_MANAGE_DEVICE_POLICY_WALLPAPER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_WIFI = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_WIFI', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_WIFI` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_WIFI => + _id_MANAGE_DEVICE_POLICY_WIFI.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_WINDOWS = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_WINDOWS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_WINDOWS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_WINDOWS => + _id_MANAGE_DEVICE_POLICY_WINDOWS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DEVICE_POLICY_WIPE_DATA = _class.staticFieldId( + r'MANAGE_DEVICE_POLICY_WIPE_DATA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DEVICE_POLICY_WIPE_DATA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DEVICE_POLICY_WIPE_DATA => + _id_MANAGE_DEVICE_POLICY_WIPE_DATA.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_DOCUMENTS = _class.staticFieldId( + r'MANAGE_DOCUMENTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_DOCUMENTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_DOCUMENTS => + _id_MANAGE_DOCUMENTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MANAGE_EXTERNAL_STORAGE = _class.staticFieldId( + r'MANAGE_EXTERNAL_STORAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_EXTERNAL_STORAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_EXTERNAL_STORAGE => + _id_MANAGE_EXTERNAL_STORAGE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MANAGE_MEDIA = _class.staticFieldId( + r'MANAGE_MEDIA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_MEDIA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_MEDIA => + _id_MANAGE_MEDIA.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MANAGE_ONGOING_CALLS = _class.staticFieldId( + r'MANAGE_ONGOING_CALLS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_ONGOING_CALLS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_ONGOING_CALLS => _id_MANAGE_ONGOING_CALLS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MANAGE_OWN_CALLS = _class.staticFieldId( + r'MANAGE_OWN_CALLS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_OWN_CALLS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_OWN_CALLS => + _id_MANAGE_OWN_CALLS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MANAGE_WIFI_INTERFACES = _class.staticFieldId( + r'MANAGE_WIFI_INTERFACES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_WIFI_INTERFACES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_WIFI_INTERFACES => _id_MANAGE_WIFI_INTERFACES + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MANAGE_WIFI_NETWORK_SELECTION = _class.staticFieldId( + r'MANAGE_WIFI_NETWORK_SELECTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MANAGE_WIFI_NETWORK_SELECTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MANAGE_WIFI_NETWORK_SELECTION => + _id_MANAGE_WIFI_NETWORK_SELECTION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MASTER_CLEAR = _class.staticFieldId( + r'MASTER_CLEAR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MASTER_CLEAR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MASTER_CLEAR => + _id_MASTER_CLEAR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MEDIA_CONTENT_CONTROL = _class.staticFieldId( + r'MEDIA_CONTENT_CONTROL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MEDIA_CONTENT_CONTROL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MEDIA_CONTENT_CONTROL => _id_MEDIA_CONTENT_CONTROL + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MEDIA_ROUTING_CONTROL = _class.staticFieldId( + r'MEDIA_ROUTING_CONTROL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MEDIA_ROUTING_CONTROL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MEDIA_ROUTING_CONTROL => _id_MEDIA_ROUTING_CONTROL + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MODIFY_AUDIO_SETTINGS = _class.staticFieldId( + r'MODIFY_AUDIO_SETTINGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MODIFY_AUDIO_SETTINGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MODIFY_AUDIO_SETTINGS => _id_MODIFY_AUDIO_SETTINGS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MODIFY_PHONE_STATE = _class.staticFieldId( + r'MODIFY_PHONE_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MODIFY_PHONE_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MODIFY_PHONE_STATE => + _id_MODIFY_PHONE_STATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MOUNT_FORMAT_FILESYSTEMS = _class.staticFieldId( + r'MOUNT_FORMAT_FILESYSTEMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MOUNT_FORMAT_FILESYSTEMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MOUNT_FORMAT_FILESYSTEMS => + _id_MOUNT_FORMAT_FILESYSTEMS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_MOUNT_UNMOUNT_FILESYSTEMS = _class.staticFieldId( + r'MOUNT_UNMOUNT_FILESYSTEMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MOUNT_UNMOUNT_FILESYSTEMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MOUNT_UNMOUNT_FILESYSTEMS => + _id_MOUNT_UNMOUNT_FILESYSTEMS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_NEARBY_WIFI_DEVICES = _class.staticFieldId( + r'NEARBY_WIFI_DEVICES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String NEARBY_WIFI_DEVICES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get NEARBY_WIFI_DEVICES => + _id_NEARBY_WIFI_DEVICES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_NFC = _class.staticFieldId(r'NFC', r'Ljava/lang/String;'); + + /// from: `static public final java.lang.String NFC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get NFC => + _id_NFC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_NFC_PREFERRED_PAYMENT_INFO = _class.staticFieldId( + r'NFC_PREFERRED_PAYMENT_INFO', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String NFC_PREFERRED_PAYMENT_INFO` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get NFC_PREFERRED_PAYMENT_INFO => + _id_NFC_PREFERRED_PAYMENT_INFO.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_NFC_TRANSACTION_EVENT = _class.staticFieldId( + r'NFC_TRANSACTION_EVENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String NFC_TRANSACTION_EVENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get NFC_TRANSACTION_EVENT => _id_NFC_TRANSACTION_EVENT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_OVERRIDE_WIFI_CONFIG = _class.staticFieldId( + r'OVERRIDE_WIFI_CONFIG', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String OVERRIDE_WIFI_CONFIG` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get OVERRIDE_WIFI_CONFIG => _id_OVERRIDE_WIFI_CONFIG + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PACKAGE_USAGE_STATS = _class.staticFieldId( + r'PACKAGE_USAGE_STATS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PACKAGE_USAGE_STATS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PACKAGE_USAGE_STATS => + _id_PACKAGE_USAGE_STATS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PERSISTENT_ACTIVITY = _class.staticFieldId( + r'PERSISTENT_ACTIVITY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PERSISTENT_ACTIVITY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PERSISTENT_ACTIVITY => + _id_PERSISTENT_ACTIVITY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_POST_NOTIFICATIONS = _class.staticFieldId( + r'POST_NOTIFICATIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String POST_NOTIFICATIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get POST_NOTIFICATIONS => + _id_POST_NOTIFICATIONS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PROCESS_OUTGOING_CALLS = _class.staticFieldId( + r'PROCESS_OUTGOING_CALLS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PROCESS_OUTGOING_CALLS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PROCESS_OUTGOING_CALLS => _id_PROCESS_OUTGOING_CALLS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PROVIDE_OWN_AUTOFILL_SUGGESTIONS = _class.staticFieldId( + r'PROVIDE_OWN_AUTOFILL_SUGGESTIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PROVIDE_OWN_AUTOFILL_SUGGESTIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PROVIDE_OWN_AUTOFILL_SUGGESTIONS => + _id_PROVIDE_OWN_AUTOFILL_SUGGESTIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_PROVIDE_REMOTE_CREDENTIALS = _class.staticFieldId( + r'PROVIDE_REMOTE_CREDENTIALS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PROVIDE_REMOTE_CREDENTIALS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PROVIDE_REMOTE_CREDENTIALS => + _id_PROVIDE_REMOTE_CREDENTIALS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_QUERY_ADVANCED_PROTECTION_MODE = _class.staticFieldId( + r'QUERY_ADVANCED_PROTECTION_MODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String QUERY_ADVANCED_PROTECTION_MODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get QUERY_ADVANCED_PROTECTION_MODE => + _id_QUERY_ADVANCED_PROTECTION_MODE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_QUERY_ALL_PACKAGES = _class.staticFieldId( + r'QUERY_ALL_PACKAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String QUERY_ALL_PACKAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get QUERY_ALL_PACKAGES => + _id_QUERY_ALL_PACKAGES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_RANGING = _class.staticFieldId( + r'RANGING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String RANGING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get RANGING => + _id_RANGING.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_ASSISTANT_APP_SEARCH_DATA = _class.staticFieldId( + r'READ_ASSISTANT_APP_SEARCH_DATA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_ASSISTANT_APP_SEARCH_DATA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_ASSISTANT_APP_SEARCH_DATA => + _id_READ_ASSISTANT_APP_SEARCH_DATA.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_READ_BASIC_PHONE_STATE = _class.staticFieldId( + r'READ_BASIC_PHONE_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_BASIC_PHONE_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_BASIC_PHONE_STATE => _id_READ_BASIC_PHONE_STATE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_CALENDAR = _class.staticFieldId( + r'READ_CALENDAR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_CALENDAR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_CALENDAR => + _id_READ_CALENDAR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_CALL_LOG = _class.staticFieldId( + r'READ_CALL_LOG', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_CALL_LOG` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_CALL_LOG => + _id_READ_CALL_LOG.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_COLOR_ZONES = _class.staticFieldId( + r'READ_COLOR_ZONES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_COLOR_ZONES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_COLOR_ZONES => + _id_READ_COLOR_ZONES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_CONTACTS = _class.staticFieldId( + r'READ_CONTACTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_CONTACTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_CONTACTS => + _id_READ_CONTACTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_DROPBOX_DATA = _class.staticFieldId( + r'READ_DROPBOX_DATA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_DROPBOX_DATA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_DROPBOX_DATA => + _id_READ_DROPBOX_DATA.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_EXTERNAL_STORAGE = _class.staticFieldId( + r'READ_EXTERNAL_STORAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_EXTERNAL_STORAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_EXTERNAL_STORAGE => _id_READ_EXTERNAL_STORAGE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_HOME_APP_SEARCH_DATA = _class.staticFieldId( + r'READ_HOME_APP_SEARCH_DATA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_HOME_APP_SEARCH_DATA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_HOME_APP_SEARCH_DATA => + _id_READ_HOME_APP_SEARCH_DATA.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_READ_INPUT_STATE = _class.staticFieldId( + r'READ_INPUT_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_INPUT_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_INPUT_STATE => + _id_READ_INPUT_STATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_LOGS = _class.staticFieldId( + r'READ_LOGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_LOGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_LOGS => + _id_READ_LOGS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_MEDIA_AUDIO = _class.staticFieldId( + r'READ_MEDIA_AUDIO', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_MEDIA_AUDIO` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_MEDIA_AUDIO => + _id_READ_MEDIA_AUDIO.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_MEDIA_IMAGES = _class.staticFieldId( + r'READ_MEDIA_IMAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_MEDIA_IMAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_MEDIA_IMAGES => + _id_READ_MEDIA_IMAGES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_MEDIA_VIDEO = _class.staticFieldId( + r'READ_MEDIA_VIDEO', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_MEDIA_VIDEO` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_MEDIA_VIDEO => + _id_READ_MEDIA_VIDEO.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_MEDIA_VISUAL_USER_SELECTED = _class.staticFieldId( + r'READ_MEDIA_VISUAL_USER_SELECTED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_MEDIA_VISUAL_USER_SELECTED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_MEDIA_VISUAL_USER_SELECTED => + _id_READ_MEDIA_VISUAL_USER_SELECTED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_READ_NEARBY_STREAMING_POLICY = _class.staticFieldId( + r'READ_NEARBY_STREAMING_POLICY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_NEARBY_STREAMING_POLICY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_NEARBY_STREAMING_POLICY => + _id_READ_NEARBY_STREAMING_POLICY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_READ_PHONE_NUMBERS = _class.staticFieldId( + r'READ_PHONE_NUMBERS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_PHONE_NUMBERS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_PHONE_NUMBERS => + _id_READ_PHONE_NUMBERS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_PHONE_STATE = _class.staticFieldId( + r'READ_PHONE_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_PHONE_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_PHONE_STATE => + _id_READ_PHONE_STATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_PRECISE_PHONE_STATE = _class.staticFieldId( + r'READ_PRECISE_PHONE_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_PRECISE_PHONE_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_PRECISE_PHONE_STATE => + _id_READ_PRECISE_PHONE_STATE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_READ_SMS = _class.staticFieldId( + r'READ_SMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_SMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_SMS => + _id_READ_SMS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_SYNC_SETTINGS = _class.staticFieldId( + r'READ_SYNC_SETTINGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_SYNC_SETTINGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_SYNC_SETTINGS => + _id_READ_SYNC_SETTINGS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_SYNC_STATS = _class.staticFieldId( + r'READ_SYNC_STATS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_SYNC_STATS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_SYNC_STATS => + _id_READ_SYNC_STATS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_SYSTEM_PREFERENCES = _class.staticFieldId( + r'READ_SYSTEM_PREFERENCES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_SYSTEM_PREFERENCES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_SYSTEM_PREFERENCES => + _id_READ_SYSTEM_PREFERENCES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_READ_VOICEMAIL = _class.staticFieldId( + r'READ_VOICEMAIL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_VOICEMAIL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_VOICEMAIL => + _id_READ_VOICEMAIL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_REBOOT = _class.staticFieldId( + r'REBOOT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REBOOT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REBOOT => + _id_REBOOT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_RECEIVE_BOOT_COMPLETED = _class.staticFieldId( + r'RECEIVE_BOOT_COMPLETED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String RECEIVE_BOOT_COMPLETED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get RECEIVE_BOOT_COMPLETED => _id_RECEIVE_BOOT_COMPLETED + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_RECEIVE_MMS = _class.staticFieldId( + r'RECEIVE_MMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String RECEIVE_MMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get RECEIVE_MMS => + _id_RECEIVE_MMS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_RECEIVE_SMS = _class.staticFieldId( + r'RECEIVE_SMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String RECEIVE_SMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get RECEIVE_SMS => + _id_RECEIVE_SMS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_RECEIVE_WAP_PUSH = _class.staticFieldId( + r'RECEIVE_WAP_PUSH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String RECEIVE_WAP_PUSH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get RECEIVE_WAP_PUSH => + _id_RECEIVE_WAP_PUSH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_RECORD_AUDIO = _class.staticFieldId( + r'RECORD_AUDIO', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String RECORD_AUDIO` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get RECORD_AUDIO => + _id_RECORD_AUDIO.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_REORDER_TASKS = _class.staticFieldId( + r'REORDER_TASKS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REORDER_TASKS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REORDER_TASKS => + _id_REORDER_TASKS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_REQUEST_COMPANION_PROFILE_APP_STREAMING = _class + .staticFieldId( + r'REQUEST_COMPANION_PROFILE_APP_STREAMING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_PROFILE_APP_STREAMING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_COMPANION_PROFILE_APP_STREAMING => + _id_REQUEST_COMPANION_PROFILE_APP_STREAMING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION = _class + .staticFieldId( + r'REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION => + _id_REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_COMPANION_PROFILE_COMPUTER = _class.staticFieldId( + r'REQUEST_COMPANION_PROFILE_COMPUTER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_PROFILE_COMPUTER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_COMPANION_PROFILE_COMPUTER => + _id_REQUEST_COMPANION_PROFILE_COMPUTER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_COMPANION_PROFILE_GLASSES = _class.staticFieldId( + r'REQUEST_COMPANION_PROFILE_GLASSES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_PROFILE_GLASSES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_COMPANION_PROFILE_GLASSES => + _id_REQUEST_COMPANION_PROFILE_GLASSES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING = _class + .staticFieldId( + r'REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING => + _id_REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_COMPANION_PROFILE_WATCH = _class.staticFieldId( + r'REQUEST_COMPANION_PROFILE_WATCH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_PROFILE_WATCH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_COMPANION_PROFILE_WATCH => + _id_REQUEST_COMPANION_PROFILE_WATCH.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_COMPANION_RUN_IN_BACKGROUND = _class.staticFieldId( + r'REQUEST_COMPANION_RUN_IN_BACKGROUND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_RUN_IN_BACKGROUND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_COMPANION_RUN_IN_BACKGROUND => + _id_REQUEST_COMPANION_RUN_IN_BACKGROUND.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_COMPANION_SELF_MANAGED = _class.staticFieldId( + r'REQUEST_COMPANION_SELF_MANAGED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_SELF_MANAGED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_COMPANION_SELF_MANAGED => + _id_REQUEST_COMPANION_SELF_MANAGED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND = + _class.staticFieldId( + r'REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? + get REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND => + _id_REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_COMPANION_USE_DATA_IN_BACKGROUND = _class + .staticFieldId( + r'REQUEST_COMPANION_USE_DATA_IN_BACKGROUND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_COMPANION_USE_DATA_IN_BACKGROUND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_COMPANION_USE_DATA_IN_BACKGROUND => + _id_REQUEST_COMPANION_USE_DATA_IN_BACKGROUND.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_DELETE_PACKAGES = _class.staticFieldId( + r'REQUEST_DELETE_PACKAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_DELETE_PACKAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_DELETE_PACKAGES => + _id_REQUEST_DELETE_PACKAGES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = _class.staticFieldId( + r'REQUEST_IGNORE_BATTERY_OPTIMIZATIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_IGNORE_BATTERY_OPTIMIZATIONS => + _id_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_INSTALL_PACKAGES = _class.staticFieldId( + r'REQUEST_INSTALL_PACKAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_INSTALL_PACKAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_INSTALL_PACKAGES => + _id_REQUEST_INSTALL_PACKAGES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE = _class + .staticFieldId( + r'REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE => + _id_REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_OBSERVE_DEVICE_UUID_PRESENCE = _class.staticFieldId( + r'REQUEST_OBSERVE_DEVICE_UUID_PRESENCE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_OBSERVE_DEVICE_UUID_PRESENCE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_OBSERVE_DEVICE_UUID_PRESENCE => + _id_REQUEST_OBSERVE_DEVICE_UUID_PRESENCE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_REQUEST_PASSWORD_COMPLEXITY = _class.staticFieldId( + r'REQUEST_PASSWORD_COMPLEXITY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String REQUEST_PASSWORD_COMPLEXITY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get REQUEST_PASSWORD_COMPLEXITY => + _id_REQUEST_PASSWORD_COMPLEXITY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_RESTART_PACKAGES = _class.staticFieldId( + r'RESTART_PACKAGES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String RESTART_PACKAGES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get RESTART_PACKAGES => + _id_RESTART_PACKAGES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_RUN_USER_INITIATED_JOBS = _class.staticFieldId( + r'RUN_USER_INITIATED_JOBS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String RUN_USER_INITIATED_JOBS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get RUN_USER_INITIATED_JOBS => + _id_RUN_USER_INITIATED_JOBS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_SCHEDULE_EXACT_ALARM = _class.staticFieldId( + r'SCHEDULE_EXACT_ALARM', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SCHEDULE_EXACT_ALARM` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SCHEDULE_EXACT_ALARM => _id_SCHEDULE_EXACT_ALARM + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SEND_RESPOND_VIA_MESSAGE = _class.staticFieldId( + r'SEND_RESPOND_VIA_MESSAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SEND_RESPOND_VIA_MESSAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SEND_RESPOND_VIA_MESSAGE => + _id_SEND_RESPOND_VIA_MESSAGE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_SEND_SMS = _class.staticFieldId( + r'SEND_SMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SEND_SMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SEND_SMS => + _id_SEND_SMS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SET_ALARM = _class.staticFieldId( + r'SET_ALARM', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_ALARM` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_ALARM => + _id_SET_ALARM.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SET_ALWAYS_FINISH = _class.staticFieldId( + r'SET_ALWAYS_FINISH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_ALWAYS_FINISH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_ALWAYS_FINISH => + _id_SET_ALWAYS_FINISH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SET_ANIMATION_SCALE = _class.staticFieldId( + r'SET_ANIMATION_SCALE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_ANIMATION_SCALE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_ANIMATION_SCALE => + _id_SET_ANIMATION_SCALE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SET_BIOMETRIC_DIALOG_ADVANCED = _class.staticFieldId( + r'SET_BIOMETRIC_DIALOG_ADVANCED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_BIOMETRIC_DIALOG_ADVANCED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_BIOMETRIC_DIALOG_ADVANCED => + _id_SET_BIOMETRIC_DIALOG_ADVANCED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_SET_DEBUG_APP = _class.staticFieldId( + r'SET_DEBUG_APP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_DEBUG_APP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_DEBUG_APP => + _id_SET_DEBUG_APP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SET_PREFERRED_APPLICATIONS = _class.staticFieldId( + r'SET_PREFERRED_APPLICATIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_PREFERRED_APPLICATIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_PREFERRED_APPLICATIONS => + _id_SET_PREFERRED_APPLICATIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_SET_PROCESS_LIMIT = _class.staticFieldId( + r'SET_PROCESS_LIMIT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_PROCESS_LIMIT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_PROCESS_LIMIT => + _id_SET_PROCESS_LIMIT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SET_TIME = _class.staticFieldId( + r'SET_TIME', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_TIME` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_TIME => + _id_SET_TIME.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SET_TIME_ZONE = _class.staticFieldId( + r'SET_TIME_ZONE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_TIME_ZONE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_TIME_ZONE => + _id_SET_TIME_ZONE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SET_WALLPAPER = _class.staticFieldId( + r'SET_WALLPAPER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_WALLPAPER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_WALLPAPER => + _id_SET_WALLPAPER.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SET_WALLPAPER_HINTS = _class.staticFieldId( + r'SET_WALLPAPER_HINTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SET_WALLPAPER_HINTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SET_WALLPAPER_HINTS => + _id_SET_WALLPAPER_HINTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SIGNAL_PERSISTENT_PROCESSES = _class.staticFieldId( + r'SIGNAL_PERSISTENT_PROCESSES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SIGNAL_PERSISTENT_PROCESSES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SIGNAL_PERSISTENT_PROCESSES => + _id_SIGNAL_PERSISTENT_PROCESSES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_SMS_FINANCIAL_TRANSACTIONS = _class.staticFieldId( + r'SMS_FINANCIAL_TRANSACTIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SMS_FINANCIAL_TRANSACTIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SMS_FINANCIAL_TRANSACTIONS => + _id_SMS_FINANCIAL_TRANSACTIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_START_FOREGROUND_SERVICES_FROM_BACKGROUND = _class + .staticFieldId( + r'START_FOREGROUND_SERVICES_FROM_BACKGROUND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String START_FOREGROUND_SERVICES_FROM_BACKGROUND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get START_FOREGROUND_SERVICES_FROM_BACKGROUND => + _id_START_FOREGROUND_SERVICES_FROM_BACKGROUND.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_START_VIEW_APP_FEATURES = _class.staticFieldId( + r'START_VIEW_APP_FEATURES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String START_VIEW_APP_FEATURES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get START_VIEW_APP_FEATURES => + _id_START_VIEW_APP_FEATURES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_START_VIEW_PERMISSION_USAGE = _class.staticFieldId( + r'START_VIEW_PERMISSION_USAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String START_VIEW_PERMISSION_USAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get START_VIEW_PERMISSION_USAGE => + _id_START_VIEW_PERMISSION_USAGE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_STATUS_BAR = _class.staticFieldId( + r'STATUS_BAR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String STATUS_BAR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get STATUS_BAR => + _id_STATUS_BAR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE = _class.staticFieldId( + r'SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE => + _id_SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_SYSTEM_ALERT_WINDOW = _class.staticFieldId( + r'SYSTEM_ALERT_WINDOW', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SYSTEM_ALERT_WINDOW` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SYSTEM_ALERT_WINDOW => + _id_SYSTEM_ALERT_WINDOW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TRANSMIT_IR = _class.staticFieldId( + r'TRANSMIT_IR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String TRANSMIT_IR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get TRANSMIT_IR => + _id_TRANSMIT_IR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TURN_SCREEN_ON = _class.staticFieldId( + r'TURN_SCREEN_ON', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String TURN_SCREEN_ON` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get TURN_SCREEN_ON => + _id_TURN_SCREEN_ON.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_TV_IMPLICIT_ENTER_PIP = _class.staticFieldId( + r'TV_IMPLICIT_ENTER_PIP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String TV_IMPLICIT_ENTER_PIP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get TV_IMPLICIT_ENTER_PIP => _id_TV_IMPLICIT_ENTER_PIP + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_UNINSTALL_SHORTCUT = _class.staticFieldId( + r'UNINSTALL_SHORTCUT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String UNINSTALL_SHORTCUT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get UNINSTALL_SHORTCUT => + _id_UNINSTALL_SHORTCUT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_UPDATE_DEVICE_STATS = _class.staticFieldId( + r'UPDATE_DEVICE_STATS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String UPDATE_DEVICE_STATS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get UPDATE_DEVICE_STATS => + _id_UPDATE_DEVICE_STATS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_UPDATE_PACKAGES_WITHOUT_USER_ACTION = _class.staticFieldId( + r'UPDATE_PACKAGES_WITHOUT_USER_ACTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String UPDATE_PACKAGES_WITHOUT_USER_ACTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get UPDATE_PACKAGES_WITHOUT_USER_ACTION => + _id_UPDATE_PACKAGES_WITHOUT_USER_ACTION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_USE_BIOMETRIC = _class.staticFieldId( + r'USE_BIOMETRIC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String USE_BIOMETRIC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get USE_BIOMETRIC => + _id_USE_BIOMETRIC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_USE_EXACT_ALARM = _class.staticFieldId( + r'USE_EXACT_ALARM', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String USE_EXACT_ALARM` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get USE_EXACT_ALARM => + _id_USE_EXACT_ALARM.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_USE_FINGERPRINT = _class.staticFieldId( + r'USE_FINGERPRINT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String USE_FINGERPRINT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get USE_FINGERPRINT => + _id_USE_FINGERPRINT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_USE_FULL_SCREEN_INTENT = _class.staticFieldId( + r'USE_FULL_SCREEN_INTENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String USE_FULL_SCREEN_INTENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get USE_FULL_SCREEN_INTENT => _id_USE_FULL_SCREEN_INTENT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER = _class.staticFieldId( + r'USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER => + _id_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_USE_SIP = _class.staticFieldId( + r'USE_SIP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String USE_SIP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get USE_SIP => + _id_USE_SIP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_UWB_RANGING = _class.staticFieldId( + r'UWB_RANGING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String UWB_RANGING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get UWB_RANGING => + _id_UWB_RANGING.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_VIBRATE = _class.staticFieldId( + r'VIBRATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String VIBRATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get VIBRATE => + _id_VIBRATE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WAKE_LOCK = _class.staticFieldId( + r'WAKE_LOCK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WAKE_LOCK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WAKE_LOCK => + _id_WAKE_LOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_APN_SETTINGS = _class.staticFieldId( + r'WRITE_APN_SETTINGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_APN_SETTINGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_APN_SETTINGS => + _id_WRITE_APN_SETTINGS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_CALENDAR = _class.staticFieldId( + r'WRITE_CALENDAR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_CALENDAR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_CALENDAR => + _id_WRITE_CALENDAR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_CALL_LOG = _class.staticFieldId( + r'WRITE_CALL_LOG', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_CALL_LOG` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_CALL_LOG => + _id_WRITE_CALL_LOG.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_CONTACTS = _class.staticFieldId( + r'WRITE_CONTACTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_CONTACTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_CONTACTS => + _id_WRITE_CONTACTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_EXTERNAL_STORAGE = _class.staticFieldId( + r'WRITE_EXTERNAL_STORAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_EXTERNAL_STORAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_EXTERNAL_STORAGE => _id_WRITE_EXTERNAL_STORAGE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_GSERVICES = _class.staticFieldId( + r'WRITE_GSERVICES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_GSERVICES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_GSERVICES => + _id_WRITE_GSERVICES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_SECURE_SETTINGS = _class.staticFieldId( + r'WRITE_SECURE_SETTINGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_SECURE_SETTINGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_SECURE_SETTINGS => _id_WRITE_SECURE_SETTINGS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_SETTINGS = _class.staticFieldId( + r'WRITE_SETTINGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_SETTINGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_SETTINGS => + _id_WRITE_SETTINGS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_SYNC_SETTINGS = _class.staticFieldId( + r'WRITE_SYNC_SETTINGS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_SYNC_SETTINGS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_SYNC_SETTINGS => + _id_WRITE_SYNC_SETTINGS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_WRITE_SYSTEM_PREFERENCES = _class.staticFieldId( + r'WRITE_SYSTEM_PREFERENCES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_SYSTEM_PREFERENCES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_SYSTEM_PREFERENCES => + _id_WRITE_SYSTEM_PREFERENCES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_WRITE_VOICEMAIL = _class.staticFieldId( + r'WRITE_VOICEMAIL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String WRITE_VOICEMAIL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get WRITE_VOICEMAIL => + _id_WRITE_VOICEMAIL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory Manifest$permission() { + return Manifest$permission.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } +} + +final class $Manifest$permission$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $Manifest$permission$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/Manifest$permission;'; + + @jni$_.internal + @core$_.override + Manifest$permission? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Manifest$permission.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Manifest$permission$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Manifest$permission$NullableType$) && + other is $Manifest$permission$NullableType$; + } +} + +final class $Manifest$permission$Type$ + extends jni$_.JType { + @jni$_.internal + const $Manifest$permission$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/Manifest$permission;'; + + @jni$_.internal + @core$_.override + Manifest$permission fromReference(jni$_.JReference reference) => + Manifest$permission.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $Manifest$permission$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Manifest$permission$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Manifest$permission$Type$) && + other is $Manifest$permission$Type$; + } +} + +/// from: `android.Manifest$permission_group` +class Manifest$permission_group extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Manifest$permission_group.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/Manifest$permission_group', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $Manifest$permission_group$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $Manifest$permission_group$Type$(); + static final _id_ACTIVITY_RECOGNITION = _class.staticFieldId( + r'ACTIVITY_RECOGNITION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String ACTIVITY_RECOGNITION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get ACTIVITY_RECOGNITION => _id_ACTIVITY_RECOGNITION + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CALENDAR = _class.staticFieldId( + r'CALENDAR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CALENDAR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CALENDAR => + _id_CALENDAR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CALL_LOG = _class.staticFieldId( + r'CALL_LOG', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CALL_LOG` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CALL_LOG => + _id_CALL_LOG.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CAMERA = _class.staticFieldId( + r'CAMERA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CAMERA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CAMERA => + _id_CAMERA.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_CONTACTS = _class.staticFieldId( + r'CONTACTS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String CONTACTS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get CONTACTS => + _id_CONTACTS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_LOCATION = _class.staticFieldId( + r'LOCATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String LOCATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get LOCATION => + _id_LOCATION.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_MICROPHONE = _class.staticFieldId( + r'MICROPHONE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String MICROPHONE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get MICROPHONE => + _id_MICROPHONE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_NEARBY_DEVICES = _class.staticFieldId( + r'NEARBY_DEVICES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String NEARBY_DEVICES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get NEARBY_DEVICES => + _id_NEARBY_DEVICES.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_NOTIFICATIONS = _class.staticFieldId( + r'NOTIFICATIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String NOTIFICATIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get NOTIFICATIONS => + _id_NOTIFICATIONS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_PHONE = _class.staticFieldId( + r'PHONE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PHONE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PHONE => + _id_PHONE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_MEDIA_AURAL = _class.staticFieldId( + r'READ_MEDIA_AURAL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_MEDIA_AURAL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_MEDIA_AURAL => + _id_READ_MEDIA_AURAL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_READ_MEDIA_VISUAL = _class.staticFieldId( + r'READ_MEDIA_VISUAL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String READ_MEDIA_VISUAL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get READ_MEDIA_VISUAL => + _id_READ_MEDIA_VISUAL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SENSORS = _class.staticFieldId( + r'SENSORS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SENSORS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SENSORS => + _id_SENSORS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_SMS = _class.staticFieldId(r'SMS', r'Ljava/lang/String;'); + + /// from: `static public final java.lang.String SMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SMS => + _id_SMS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_STORAGE = _class.staticFieldId( + r'STORAGE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String STORAGE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get STORAGE => + _id_STORAGE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory Manifest$permission_group() { + return Manifest$permission_group.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } +} + +final class $Manifest$permission_group$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $Manifest$permission_group$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/Manifest$permission_group;'; + + @jni$_.internal + @core$_.override + Manifest$permission_group? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : Manifest$permission_group.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Manifest$permission_group$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Manifest$permission_group$NullableType$) && + other is $Manifest$permission_group$NullableType$; + } +} + +final class $Manifest$permission_group$Type$ + extends jni$_.JType { + @jni$_.internal + const $Manifest$permission_group$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/Manifest$permission_group;'; + + @jni$_.internal + @core$_.override + Manifest$permission_group fromReference(jni$_.JReference reference) => + Manifest$permission_group.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $Manifest$permission_group$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Manifest$permission_group$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Manifest$permission_group$Type$) && + other is $Manifest$permission_group$Type$; + } +} + +/// from: `android.Manifest` +class Manifest extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Manifest.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName(r'android/Manifest'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Manifest$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Manifest$Type$(); + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory Manifest() { + return Manifest.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } +} + +final class $Manifest$NullableType$ extends jni$_.JType { + @jni$_.internal + const $Manifest$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/Manifest;'; + + @jni$_.internal + @core$_.override + Manifest? fromReference(jni$_.JReference reference) => + reference.isNull ? null : Manifest.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Manifest$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Manifest$NullableType$) && + other is $Manifest$NullableType$; + } +} + +final class $Manifest$Type$ extends jni$_.JType { + @jni$_.internal + const $Manifest$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/Manifest;'; + + @jni$_.internal + @core$_.override + Manifest fromReference(jni$_.JReference reference) => + Manifest.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Manifest$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Manifest$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Manifest$Type$) && other is $Manifest$Type$; + } +} + +/// from: `android.content.pm.PackageManager$ApplicationInfoFlags` +class PackageManager$ApplicationInfoFlags extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + PackageManager$ApplicationInfoFlags.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/pm/PackageManager$ApplicationInfoFlags', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $PackageManager$ApplicationInfoFlags$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $PackageManager$ApplicationInfoFlags$Type$(); + static final _id_getValue = _class.instanceMethodId(r'getValue', r'()J'); + + static final _getValue = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallLongMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public long getValue()` + int getValue() { + return _getValue( + reference.pointer, + _id_getValue as jni$_.JMethodIDPtr, + ).long; + } + + static final _id_of = _class.staticMethodId( + r'of', + r'(J)Landroid/content/pm/PackageManager$ApplicationInfoFlags;', + ); + + static final _of = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int64,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `static public android.content.pm.PackageManager$ApplicationInfoFlags of(long j)` + /// The returned object must be released after use, by calling the [release] method. + static PackageManager$ApplicationInfoFlags? of(int j) { + return _of( + _class.reference.pointer, + _id_of as jni$_.JMethodIDPtr, + j, + ).object( + const $PackageManager$ApplicationInfoFlags$NullableType$(), + ); + } +} + +final class $PackageManager$ApplicationInfoFlags$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$ApplicationInfoFlags$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$ApplicationInfoFlags;'; + + @jni$_.internal + @core$_.override + PackageManager$ApplicationInfoFlags? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : PackageManager$ApplicationInfoFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($PackageManager$ApplicationInfoFlags$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($PackageManager$ApplicationInfoFlags$NullableType$) && + other is $PackageManager$ApplicationInfoFlags$NullableType$; + } +} + +final class $PackageManager$ApplicationInfoFlags$Type$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$ApplicationInfoFlags$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$ApplicationInfoFlags;'; + + @jni$_.internal + @core$_.override + PackageManager$ApplicationInfoFlags fromReference( + jni$_.JReference reference, + ) => PackageManager$ApplicationInfoFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $PackageManager$ApplicationInfoFlags$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$ApplicationInfoFlags$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($PackageManager$ApplicationInfoFlags$Type$) && + other is $PackageManager$ApplicationInfoFlags$Type$; + } +} + +/// from: `android.content.pm.PackageManager$ComponentEnabledSetting` +class PackageManager$ComponentEnabledSetting extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + PackageManager$ComponentEnabledSetting.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/pm/PackageManager$ComponentEnabledSetting', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType + nullableType = $PackageManager$ComponentEnabledSetting$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $PackageManager$ComponentEnabledSetting$Type$(); + static final _id_CREATOR = _class.staticFieldId( + r'CREATOR', + r'Landroid/os/Parcelable$Creator;', + ); + + /// from: `static public final android.os.Parcelable$Creator CREATOR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? get CREATOR => + _id_CREATOR.get(_class, const jni$_.$JObject$NullableType$()); + + static final _id_new$ = _class.constructorId( + r'(Landroid/content/ComponentName;II)V', + ); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Int32, jni$_.Int32) + >, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + int, + ) + >(); + + /// from: `public void (android.content.ComponentName componentName, int i, int i1)` + /// The returned object must be released after use, by calling the [release] method. + factory PackageManager$ComponentEnabledSetting( + jni$_.JObject? componentName, + int i, + int i1, + ) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return PackageManager$ComponentEnabledSetting.fromReference( + _new$( + _class.reference.pointer, + _id_new$ as jni$_.JMethodIDPtr, + _$componentName.pointer, + i, + i1, + ).reference, + ); + } + + static final _id_describeContents = _class.instanceMethodId( + r'describeContents', + r'()I', + ); + + static final _describeContents = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int describeContents()` + int describeContents() { + return _describeContents( + reference.pointer, + _id_describeContents as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_getComponentName = _class.instanceMethodId( + r'getComponentName', + r'()Landroid/content/ComponentName;', + ); + + static final _getComponentName = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.content.ComponentName getComponentName()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getComponentName() { + return _getComponentName( + reference.pointer, + _id_getComponentName as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getEnabledFlags = _class.instanceMethodId( + r'getEnabledFlags', + r'()I', + ); + + static final _getEnabledFlags = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int getEnabledFlags()` + int getEnabledFlags() { + return _getEnabledFlags( + reference.pointer, + _id_getEnabledFlags as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_getEnabledState = _class.instanceMethodId( + r'getEnabledState', + r'()I', + ); + + static final _getEnabledState = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int getEnabledState()` + int getEnabledState() { + return _getEnabledState( + reference.pointer, + _id_getEnabledState as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_writeToParcel = _class.instanceMethodId( + r'writeToParcel', + r'(Landroid/os/Parcel;I)V', + ); + + static final _writeToParcel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public void writeToParcel(android.os.Parcel parcel, int i)` + void writeToParcel(jni$_.JObject? parcel, int i) { + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _writeToParcel( + reference.pointer, + _id_writeToParcel as jni$_.JMethodIDPtr, + _$parcel.pointer, + i, + ).check(); + } +} + +final class $PackageManager$ComponentEnabledSetting$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$ComponentEnabledSetting$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$ComponentEnabledSetting;'; + + @jni$_.internal + @core$_.override + PackageManager$ComponentEnabledSetting? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : PackageManager$ComponentEnabledSetting.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($PackageManager$ComponentEnabledSetting$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($PackageManager$ComponentEnabledSetting$NullableType$) && + other is $PackageManager$ComponentEnabledSetting$NullableType$; + } +} + +final class $PackageManager$ComponentEnabledSetting$Type$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$ComponentEnabledSetting$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$ComponentEnabledSetting;'; + + @jni$_.internal + @core$_.override + PackageManager$ComponentEnabledSetting fromReference( + jni$_.JReference reference, + ) => PackageManager$ComponentEnabledSetting.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $PackageManager$ComponentEnabledSetting$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$ComponentEnabledSetting$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($PackageManager$ComponentEnabledSetting$Type$) && + other is $PackageManager$ComponentEnabledSetting$Type$; + } +} + +/// from: `android.content.pm.PackageManager$ComponentInfoFlags` +class PackageManager$ComponentInfoFlags extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + PackageManager$ComponentInfoFlags.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/pm/PackageManager$ComponentInfoFlags', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $PackageManager$ComponentInfoFlags$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $PackageManager$ComponentInfoFlags$Type$(); + static final _id_getValue = _class.instanceMethodId(r'getValue', r'()J'); + + static final _getValue = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallLongMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public long getValue()` + int getValue() { + return _getValue( + reference.pointer, + _id_getValue as jni$_.JMethodIDPtr, + ).long; + } + + static final _id_of = _class.staticMethodId( + r'of', + r'(J)Landroid/content/pm/PackageManager$ComponentInfoFlags;', + ); + + static final _of = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int64,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `static public android.content.pm.PackageManager$ComponentInfoFlags of(long j)` + /// The returned object must be released after use, by calling the [release] method. + static PackageManager$ComponentInfoFlags? of(int j) { + return _of( + _class.reference.pointer, + _id_of as jni$_.JMethodIDPtr, + j, + ).object( + const $PackageManager$ComponentInfoFlags$NullableType$(), + ); + } +} + +final class $PackageManager$ComponentInfoFlags$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$ComponentInfoFlags$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$ComponentInfoFlags;'; + + @jni$_.internal + @core$_.override + PackageManager$ComponentInfoFlags? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : PackageManager$ComponentInfoFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($PackageManager$ComponentInfoFlags$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($PackageManager$ComponentInfoFlags$NullableType$) && + other is $PackageManager$ComponentInfoFlags$NullableType$; + } +} + +final class $PackageManager$ComponentInfoFlags$Type$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$ComponentInfoFlags$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$ComponentInfoFlags;'; + + @jni$_.internal + @core$_.override + PackageManager$ComponentInfoFlags fromReference(jni$_.JReference reference) => + PackageManager$ComponentInfoFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $PackageManager$ComponentInfoFlags$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$ComponentInfoFlags$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($PackageManager$ComponentInfoFlags$Type$) && + other is $PackageManager$ComponentInfoFlags$Type$; + } +} + +/// from: `android.content.pm.PackageManager$NameNotFoundException` +class PackageManager$NameNotFoundException extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + PackageManager$NameNotFoundException.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/pm/PackageManager$NameNotFoundException', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $PackageManager$NameNotFoundException$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $PackageManager$NameNotFoundException$Type$(); + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory PackageManager$NameNotFoundException() { + return PackageManager$NameNotFoundException.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr).reference, + ); + } + + static final _id_new$1 = _class.constructorId(r'(Ljava/lang/String;)V'); + + static final _new$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void (java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + factory PackageManager$NameNotFoundException.new$1(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return PackageManager$NameNotFoundException.fromReference( + _new$1( + _class.reference.pointer, + _id_new$1 as jni$_.JMethodIDPtr, + _$string.pointer, + ).reference, + ); + } +} + +final class $PackageManager$NameNotFoundException$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$NameNotFoundException$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$NameNotFoundException;'; + + @jni$_.internal + @core$_.override + PackageManager$NameNotFoundException? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : PackageManager$NameNotFoundException.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($PackageManager$NameNotFoundException$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($PackageManager$NameNotFoundException$NullableType$) && + other is $PackageManager$NameNotFoundException$NullableType$; + } +} + +final class $PackageManager$NameNotFoundException$Type$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$NameNotFoundException$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$NameNotFoundException;'; + + @jni$_.internal + @core$_.override + PackageManager$NameNotFoundException fromReference( + jni$_.JReference reference, + ) => PackageManager$NameNotFoundException.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $PackageManager$NameNotFoundException$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$NameNotFoundException$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($PackageManager$NameNotFoundException$Type$) && + other is $PackageManager$NameNotFoundException$Type$; + } +} + +/// from: `android.content.pm.PackageManager$OnChecksumsReadyListener` +class PackageManager$OnChecksumsReadyListener extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + PackageManager$OnChecksumsReadyListener.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/pm/PackageManager$OnChecksumsReadyListener', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType + nullableType = $PackageManager$OnChecksumsReadyListener$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $PackageManager$OnChecksumsReadyListener$Type$(); + static final _id_onChecksumsReady = _class.instanceMethodId( + r'onChecksumsReady', + r'(Ljava/util/List;)V', + ); + + static final _onChecksumsReady = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void onChecksumsReady(java.util.List list)` + void onChecksumsReady(jni$_.JList? list) { + final _$list = list?.reference ?? jni$_.jNullReference; + _onChecksumsReady( + reference.pointer, + _id_onChecksumsReady as jni$_.JMethodIDPtr, + _$list.pointer, + ).check(); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map + _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses(0, descriptor.address, args.address), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r'onChecksumsReady(Ljava/util/List;)V') { + _$impls[$p]!.onChecksumsReady( + $a![0]?.as( + const jni$_.$JList$Type$( + jni$_.$JObject$NullableType$(), + ), + releaseOriginal: true, + ), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $PackageManager$OnChecksumsReadyListener $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'android.content.pm.PackageManager$OnChecksumsReadyListener', + $p, + _$invokePointer, + [ + if ($impl.onChecksumsReady$async) + r'onChecksumsReady(Ljava/util/List;)V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory PackageManager$OnChecksumsReadyListener.implement( + $PackageManager$OnChecksumsReadyListener $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return PackageManager$OnChecksumsReadyListener.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $PackageManager$OnChecksumsReadyListener { + factory $PackageManager$OnChecksumsReadyListener({ + required void Function(jni$_.JList? list) onChecksumsReady, + bool onChecksumsReady$async, + }) = _$PackageManager$OnChecksumsReadyListener; + + void onChecksumsReady(jni$_.JList? list); + bool get onChecksumsReady$async => false; +} + +final class _$PackageManager$OnChecksumsReadyListener + with $PackageManager$OnChecksumsReadyListener { + _$PackageManager$OnChecksumsReadyListener({ + required void Function(jni$_.JList? list) onChecksumsReady, + this.onChecksumsReady$async = false, + }) : _onChecksumsReady = onChecksumsReady; + + final void Function(jni$_.JList? list) _onChecksumsReady; + final bool onChecksumsReady$async; + + void onChecksumsReady(jni$_.JList? list) { + return _onChecksumsReady(list); + } +} + +final class $PackageManager$OnChecksumsReadyListener$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$OnChecksumsReadyListener$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$OnChecksumsReadyListener;'; + + @jni$_.internal + @core$_.override + PackageManager$OnChecksumsReadyListener? fromReference( + jni$_.JReference reference, + ) => reference.isNull + ? null + : PackageManager$OnChecksumsReadyListener.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($PackageManager$OnChecksumsReadyListener$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($PackageManager$OnChecksumsReadyListener$NullableType$) && + other is $PackageManager$OnChecksumsReadyListener$NullableType$; + } +} + +final class $PackageManager$OnChecksumsReadyListener$Type$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$OnChecksumsReadyListener$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$OnChecksumsReadyListener;'; + + @jni$_.internal + @core$_.override + PackageManager$OnChecksumsReadyListener fromReference( + jni$_.JReference reference, + ) => PackageManager$OnChecksumsReadyListener.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $PackageManager$OnChecksumsReadyListener$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$OnChecksumsReadyListener$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($PackageManager$OnChecksumsReadyListener$Type$) && + other is $PackageManager$OnChecksumsReadyListener$Type$; + } +} + +/// from: `android.content.pm.PackageManager$PackageInfoFlags` +class PackageManager$PackageInfoFlags extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + PackageManager$PackageInfoFlags.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/pm/PackageManager$PackageInfoFlags', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $PackageManager$PackageInfoFlags$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $PackageManager$PackageInfoFlags$Type$(); + static final _id_getValue = _class.instanceMethodId(r'getValue', r'()J'); + + static final _getValue = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallLongMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public long getValue()` + int getValue() { + return _getValue( + reference.pointer, + _id_getValue as jni$_.JMethodIDPtr, + ).long; + } + + static final _id_of = _class.staticMethodId( + r'of', + r'(J)Landroid/content/pm/PackageManager$PackageInfoFlags;', + ); + + static final _of = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int64,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `static public android.content.pm.PackageManager$PackageInfoFlags of(long j)` + /// The returned object must be released after use, by calling the [release] method. + static PackageManager$PackageInfoFlags? of(int j) { + return _of( + _class.reference.pointer, + _id_of as jni$_.JMethodIDPtr, + j, + ).object( + const $PackageManager$PackageInfoFlags$NullableType$(), + ); + } +} + +final class $PackageManager$PackageInfoFlags$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$PackageInfoFlags$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$PackageInfoFlags;'; + + @jni$_.internal + @core$_.override + PackageManager$PackageInfoFlags? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : PackageManager$PackageInfoFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$PackageInfoFlags$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($PackageManager$PackageInfoFlags$NullableType$) && + other is $PackageManager$PackageInfoFlags$NullableType$; + } +} + +final class $PackageManager$PackageInfoFlags$Type$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$PackageInfoFlags$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$PackageInfoFlags;'; + + @jni$_.internal + @core$_.override + PackageManager$PackageInfoFlags fromReference(jni$_.JReference reference) => + PackageManager$PackageInfoFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $PackageManager$PackageInfoFlags$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$PackageInfoFlags$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($PackageManager$PackageInfoFlags$Type$) && + other is $PackageManager$PackageInfoFlags$Type$; + } +} + +/// from: `android.content.pm.PackageManager$Property` +class PackageManager$Property extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + PackageManager$Property.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/pm/PackageManager$Property', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $PackageManager$Property$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $PackageManager$Property$Type$(); + static final _id_CREATOR = _class.staticFieldId( + r'CREATOR', + r'Landroid/os/Parcelable$Creator;', + ); + + /// from: `static public final android.os.Parcelable$Creator CREATOR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? get CREATOR => + _id_CREATOR.get(_class, const jni$_.$JObject$NullableType$()); + + static final _id_describeContents = _class.instanceMethodId( + r'describeContents', + r'()I', + ); + + static final _describeContents = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int describeContents()` + int describeContents() { + return _describeContents( + reference.pointer, + _id_describeContents as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_equals = _class.instanceMethodId( + r'equals', + r'(Ljava/lang/Object;)Z', + ); + + static final _equals = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean equals(java.lang.Object object)` + bool equals(jni$_.JObject? object) { + final _$object = object?.reference ?? jni$_.jNullReference; + return _equals( + reference.pointer, + _id_equals as jni$_.JMethodIDPtr, + _$object.pointer, + ).boolean; + } + + static final _id_getBoolean = _class.instanceMethodId(r'getBoolean', r'()Z'); + + static final _getBoolean = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean getBoolean()` + bool getBoolean() { + return _getBoolean( + reference.pointer, + _id_getBoolean as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_getClassName = _class.instanceMethodId( + r'getClassName', + r'()Ljava/lang/String;', + ); + + static final _getClassName = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getClassName()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getClassName() { + return _getClassName( + reference.pointer, + _id_getClassName as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getFloat = _class.instanceMethodId(r'getFloat', r'()F'); + + static final _getFloat = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallFloatMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public float getFloat()` + double getFloat() { + return _getFloat( + reference.pointer, + _id_getFloat as jni$_.JMethodIDPtr, + ).float; + } + + static final _id_getInteger = _class.instanceMethodId(r'getInteger', r'()I'); + + static final _getInteger = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int getInteger()` + int getInteger() { + return _getInteger( + reference.pointer, + _id_getInteger as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_getName = _class.instanceMethodId( + r'getName', + r'()Ljava/lang/String;', + ); + + static final _getName = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getName()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getName() { + return _getName( + reference.pointer, + _id_getName as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getPackageName = _class.instanceMethodId( + r'getPackageName', + r'()Ljava/lang/String;', + ); + + static final _getPackageName = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getPackageName()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getPackageName() { + return _getPackageName( + reference.pointer, + _id_getPackageName as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getResourceId = _class.instanceMethodId( + r'getResourceId', + r'()I', + ); + + static final _getResourceId = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int getResourceId()` + int getResourceId() { + return _getResourceId( + reference.pointer, + _id_getResourceId as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_getString = _class.instanceMethodId( + r'getString', + r'()Ljava/lang/String;', + ); + + static final _getString = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.String getString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getString() { + return _getString( + reference.pointer, + _id_getString as jni$_.JMethodIDPtr, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_hashCode$1 = _class.instanceMethodId(r'hashCode', r'()I'); + + static final _hashCode$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public int hashCode()` + int hashCode$1() { + return _hashCode$1( + reference.pointer, + _id_hashCode$1 as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_isBoolean = _class.instanceMethodId(r'isBoolean', r'()Z'); + + static final _isBoolean = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean isBoolean()` + bool isBoolean() { + return _isBoolean( + reference.pointer, + _id_isBoolean as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_isFloat = _class.instanceMethodId(r'isFloat', r'()Z'); + + static final _isFloat = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean isFloat()` + bool isFloat() { + return _isFloat( + reference.pointer, + _id_isFloat as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_isInteger = _class.instanceMethodId(r'isInteger', r'()Z'); + + static final _isInteger = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean isInteger()` + bool isInteger() { + return _isInteger( + reference.pointer, + _id_isInteger as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_isResourceId = _class.instanceMethodId( + r'isResourceId', + r'()Z', + ); + + static final _isResourceId = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean isResourceId()` + bool isResourceId() { + return _isResourceId( + reference.pointer, + _id_isResourceId as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_isString = _class.instanceMethodId(r'isString', r'()Z'); + + static final _isString = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean isString()` + bool isString() { + return _isString( + reference.pointer, + _id_isString as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_writeToParcel = _class.instanceMethodId( + r'writeToParcel', + r'(Landroid/os/Parcel;I)V', + ); + + static final _writeToParcel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public void writeToParcel(android.os.Parcel parcel, int i)` + void writeToParcel(jni$_.JObject? parcel, int i) { + final _$parcel = parcel?.reference ?? jni$_.jNullReference; + _writeToParcel( + reference.pointer, + _id_writeToParcel as jni$_.JMethodIDPtr, + _$parcel.pointer, + i, + ).check(); + } +} + +final class $PackageManager$Property$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$Property$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/pm/PackageManager$Property;'; + + @jni$_.internal + @core$_.override + PackageManager$Property? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : PackageManager$Property.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$Property$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($PackageManager$Property$NullableType$) && + other is $PackageManager$Property$NullableType$; + } +} + +final class $PackageManager$Property$Type$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$Property$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/pm/PackageManager$Property;'; + + @jni$_.internal + @core$_.override + PackageManager$Property fromReference(jni$_.JReference reference) => + PackageManager$Property.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $PackageManager$Property$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$Property$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($PackageManager$Property$Type$) && + other is $PackageManager$Property$Type$; + } +} + +/// from: `android.content.pm.PackageManager$ResolveInfoFlags` +class PackageManager$ResolveInfoFlags extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + PackageManager$ResolveInfoFlags.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/pm/PackageManager$ResolveInfoFlags', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $PackageManager$ResolveInfoFlags$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $PackageManager$ResolveInfoFlags$Type$(); + static final _id_getValue = _class.instanceMethodId(r'getValue', r'()J'); + + static final _getValue = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallLongMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public long getValue()` + int getValue() { + return _getValue( + reference.pointer, + _id_getValue as jni$_.JMethodIDPtr, + ).long; + } + + static final _id_of = _class.staticMethodId( + r'of', + r'(J)Landroid/content/pm/PackageManager$ResolveInfoFlags;', + ); + + static final _of = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int64,)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `static public android.content.pm.PackageManager$ResolveInfoFlags of(long j)` + /// The returned object must be released after use, by calling the [release] method. + static PackageManager$ResolveInfoFlags? of(int j) { + return _of( + _class.reference.pointer, + _id_of as jni$_.JMethodIDPtr, + j, + ).object( + const $PackageManager$ResolveInfoFlags$NullableType$(), + ); + } +} + +final class $PackageManager$ResolveInfoFlags$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$ResolveInfoFlags$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$ResolveInfoFlags;'; + + @jni$_.internal + @core$_.override + PackageManager$ResolveInfoFlags? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : PackageManager$ResolveInfoFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$ResolveInfoFlags$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($PackageManager$ResolveInfoFlags$NullableType$) && + other is $PackageManager$ResolveInfoFlags$NullableType$; + } +} + +final class $PackageManager$ResolveInfoFlags$Type$ + extends jni$_.JType { + @jni$_.internal + const $PackageManager$ResolveInfoFlags$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Landroid/content/pm/PackageManager$ResolveInfoFlags;'; + + @jni$_.internal + @core$_.override + PackageManager$ResolveInfoFlags fromReference(jni$_.JReference reference) => + PackageManager$ResolveInfoFlags.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $PackageManager$ResolveInfoFlags$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$ResolveInfoFlags$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($PackageManager$ResolveInfoFlags$Type$) && + other is $PackageManager$ResolveInfoFlags$Type$; + } +} + +/// from: `android.content.pm.PackageManager` +class PackageManager extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + PackageManager.fromReference(jni$_.JReference reference) + : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'android/content/pm/PackageManager', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $PackageManager$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $PackageManager$Type$(); + + /// from: `static public final int CERT_INPUT_RAW_X509` + static const CERT_INPUT_RAW_X509 = 0; + + /// from: `static public final int CERT_INPUT_SHA256` + static const CERT_INPUT_SHA256 = 1; + + /// from: `static public final int COMPONENT_ENABLED_STATE_DEFAULT` + static const COMPONENT_ENABLED_STATE_DEFAULT = 0; + + /// from: `static public final int COMPONENT_ENABLED_STATE_DISABLED` + static const COMPONENT_ENABLED_STATE_DISABLED = 2; + + /// from: `static public final int COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED` + static const COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED = 4; + + /// from: `static public final int COMPONENT_ENABLED_STATE_DISABLED_USER` + static const COMPONENT_ENABLED_STATE_DISABLED_USER = 3; + + /// from: `static public final int COMPONENT_ENABLED_STATE_ENABLED` + static const COMPONENT_ENABLED_STATE_ENABLED = 1; + + /// from: `static public final int DELETE_ARCHIVE` + static const DELETE_ARCHIVE = 16; + + /// from: `static public final int DONT_KILL_APP` + static const DONT_KILL_APP = 1; + static final _id_EXTRA_VERIFICATION_ID = _class.staticFieldId( + r'EXTRA_VERIFICATION_ID', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_VERIFICATION_ID` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_VERIFICATION_ID => _id_EXTRA_VERIFICATION_ID + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_EXTRA_VERIFICATION_RESULT = _class.staticFieldId( + r'EXTRA_VERIFICATION_RESULT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String EXTRA_VERIFICATION_RESULT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get EXTRA_VERIFICATION_RESULT => + _id_EXTRA_VERIFICATION_RESULT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS = _class + .staticFieldId( + r'FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS => + _id_FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_APP_WIDGETS = _class.staticFieldId( + r'FEATURE_APP_WIDGETS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_APP_WIDGETS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_APP_WIDGETS => + _id_FEATURE_APP_WIDGETS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_AUDIO_LOW_LATENCY = _class.staticFieldId( + r'FEATURE_AUDIO_LOW_LATENCY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_AUDIO_LOW_LATENCY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_AUDIO_LOW_LATENCY => + _id_FEATURE_AUDIO_LOW_LATENCY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_AUDIO_OUTPUT = _class.staticFieldId( + r'FEATURE_AUDIO_OUTPUT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_AUDIO_OUTPUT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_AUDIO_OUTPUT => _id_FEATURE_AUDIO_OUTPUT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_AUDIO_PRO = _class.staticFieldId( + r'FEATURE_AUDIO_PRO', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_AUDIO_PRO` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_AUDIO_PRO => + _id_FEATURE_AUDIO_PRO.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY = _class + .staticFieldId( + r'FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY => + _id_FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_AUTOFILL = _class.staticFieldId( + r'FEATURE_AUTOFILL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_AUTOFILL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_AUTOFILL => + _id_FEATURE_AUTOFILL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_AUTOMOTIVE = _class.staticFieldId( + r'FEATURE_AUTOMOTIVE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_AUTOMOTIVE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_AUTOMOTIVE => + _id_FEATURE_AUTOMOTIVE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_BACKUP = _class.staticFieldId( + r'FEATURE_BACKUP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_BACKUP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_BACKUP => + _id_FEATURE_BACKUP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_BLUETOOTH = _class.staticFieldId( + r'FEATURE_BLUETOOTH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_BLUETOOTH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_BLUETOOTH => + _id_FEATURE_BLUETOOTH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_BLUETOOTH_LE = _class.staticFieldId( + r'FEATURE_BLUETOOTH_LE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_BLUETOOTH_LE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_BLUETOOTH_LE => _id_FEATURE_BLUETOOTH_LE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING = _class.staticFieldId( + r'FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING => + _id_FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CAMERA = _class.staticFieldId( + r'FEATURE_CAMERA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA => + _id_FEATURE_CAMERA.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_CAMERA_ANY = _class.staticFieldId( + r'FEATURE_CAMERA_ANY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_ANY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_ANY => + _id_FEATURE_CAMERA_ANY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_CAMERA_AR = _class.staticFieldId( + r'FEATURE_CAMERA_AR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_AR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_AR => + _id_FEATURE_CAMERA_AR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_CAMERA_AUTOFOCUS = _class.staticFieldId( + r'FEATURE_CAMERA_AUTOFOCUS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_AUTOFOCUS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_AUTOFOCUS => + _id_FEATURE_CAMERA_AUTOFOCUS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CAMERA_CAPABILITY_MANUAL_POST_PROCESSING = _class + .staticFieldId( + r'FEATURE_CAMERA_CAPABILITY_MANUAL_POST_PROCESSING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_CAPABILITY_MANUAL_POST_PROCESSING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_CAPABILITY_MANUAL_POST_PROCESSING => + _id_FEATURE_CAMERA_CAPABILITY_MANUAL_POST_PROCESSING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CAMERA_CAPABILITY_MANUAL_SENSOR = _class + .staticFieldId( + r'FEATURE_CAMERA_CAPABILITY_MANUAL_SENSOR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_CAPABILITY_MANUAL_SENSOR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_CAPABILITY_MANUAL_SENSOR => + _id_FEATURE_CAMERA_CAPABILITY_MANUAL_SENSOR.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CAMERA_CAPABILITY_RAW = _class.staticFieldId( + r'FEATURE_CAMERA_CAPABILITY_RAW', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_CAPABILITY_RAW` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_CAPABILITY_RAW => + _id_FEATURE_CAMERA_CAPABILITY_RAW.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CAMERA_CONCURRENT = _class.staticFieldId( + r'FEATURE_CAMERA_CONCURRENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_CONCURRENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_CONCURRENT => + _id_FEATURE_CAMERA_CONCURRENT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CAMERA_EXTERNAL = _class.staticFieldId( + r'FEATURE_CAMERA_EXTERNAL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_EXTERNAL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_EXTERNAL => + _id_FEATURE_CAMERA_EXTERNAL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CAMERA_FLASH = _class.staticFieldId( + r'FEATURE_CAMERA_FLASH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_FLASH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_FLASH => _id_FEATURE_CAMERA_FLASH + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_CAMERA_FRONT = _class.staticFieldId( + r'FEATURE_CAMERA_FRONT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_FRONT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_FRONT => _id_FEATURE_CAMERA_FRONT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_CAMERA_LEVEL_FULL = _class.staticFieldId( + r'FEATURE_CAMERA_LEVEL_FULL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CAMERA_LEVEL_FULL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CAMERA_LEVEL_FULL => + _id_FEATURE_CAMERA_LEVEL_FULL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CANT_SAVE_STATE = _class.staticFieldId( + r'FEATURE_CANT_SAVE_STATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CANT_SAVE_STATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CANT_SAVE_STATE => + _id_FEATURE_CANT_SAVE_STATE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_COMPANION_DEVICE_SETUP = _class.staticFieldId( + r'FEATURE_COMPANION_DEVICE_SETUP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_COMPANION_DEVICE_SETUP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_COMPANION_DEVICE_SETUP => + _id_FEATURE_COMPANION_DEVICE_SETUP.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CONNECTION_SERVICE = _class.staticFieldId( + r'FEATURE_CONNECTION_SERVICE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CONNECTION_SERVICE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CONNECTION_SERVICE => + _id_FEATURE_CONNECTION_SERVICE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_CONSUMER_IR = _class.staticFieldId( + r'FEATURE_CONSUMER_IR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CONSUMER_IR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CONSUMER_IR => + _id_FEATURE_CONSUMER_IR.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_CONTROLS = _class.staticFieldId( + r'FEATURE_CONTROLS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CONTROLS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CONTROLS => + _id_FEATURE_CONTROLS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_CREDENTIALS = _class.staticFieldId( + r'FEATURE_CREDENTIALS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_CREDENTIALS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_CREDENTIALS => + _id_FEATURE_CREDENTIALS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_DEVICE_ADMIN = _class.staticFieldId( + r'FEATURE_DEVICE_ADMIN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_DEVICE_ADMIN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_DEVICE_ADMIN => _id_FEATURE_DEVICE_ADMIN + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_DEVICE_LOCK = _class.staticFieldId( + r'FEATURE_DEVICE_LOCK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_DEVICE_LOCK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_DEVICE_LOCK => + _id_FEATURE_DEVICE_LOCK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_EMBEDDED = _class.staticFieldId( + r'FEATURE_EMBEDDED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_EMBEDDED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_EMBEDDED => + _id_FEATURE_EMBEDDED.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_ETHERNET = _class.staticFieldId( + r'FEATURE_ETHERNET', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_ETHERNET` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_ETHERNET => + _id_FEATURE_ETHERNET.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_EXPANDED_PICTURE_IN_PICTURE = _class.staticFieldId( + r'FEATURE_EXPANDED_PICTURE_IN_PICTURE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_EXPANDED_PICTURE_IN_PICTURE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_EXPANDED_PICTURE_IN_PICTURE => + _id_FEATURE_EXPANDED_PICTURE_IN_PICTURE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_FACE = _class.staticFieldId( + r'FEATURE_FACE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_FACE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_FACE => + _id_FEATURE_FACE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_FAKETOUCH = _class.staticFieldId( + r'FEATURE_FAKETOUCH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_FAKETOUCH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_FAKETOUCH => + _id_FEATURE_FAKETOUCH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT = _class.staticFieldId( + r'FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT => + _id_FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_FAKETOUCH_MULTITOUCH_JAZZHAND = _class.staticFieldId( + r'FEATURE_FAKETOUCH_MULTITOUCH_JAZZHAND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_FAKETOUCH_MULTITOUCH_JAZZHAND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_FAKETOUCH_MULTITOUCH_JAZZHAND => + _id_FEATURE_FAKETOUCH_MULTITOUCH_JAZZHAND.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_FINGERPRINT = _class.staticFieldId( + r'FEATURE_FINGERPRINT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_FINGERPRINT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_FINGERPRINT => + _id_FEATURE_FINGERPRINT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_FREEFORM_WINDOW_MANAGEMENT = _class.staticFieldId( + r'FEATURE_FREEFORM_WINDOW_MANAGEMENT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_FREEFORM_WINDOW_MANAGEMENT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_FREEFORM_WINDOW_MANAGEMENT => + _id_FEATURE_FREEFORM_WINDOW_MANAGEMENT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_GAMEPAD = _class.staticFieldId( + r'FEATURE_GAMEPAD', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_GAMEPAD` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_GAMEPAD => + _id_FEATURE_GAMEPAD.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_HARDWARE_KEYSTORE = _class.staticFieldId( + r'FEATURE_HARDWARE_KEYSTORE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_HARDWARE_KEYSTORE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_HARDWARE_KEYSTORE => + _id_FEATURE_HARDWARE_KEYSTORE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_HIFI_SENSORS = _class.staticFieldId( + r'FEATURE_HIFI_SENSORS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_HIFI_SENSORS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_HIFI_SENSORS => _id_FEATURE_HIFI_SENSORS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_HOME_SCREEN = _class.staticFieldId( + r'FEATURE_HOME_SCREEN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_HOME_SCREEN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_HOME_SCREEN => + _id_FEATURE_HOME_SCREEN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_IDENTITY_CREDENTIAL_HARDWARE = _class.staticFieldId( + r'FEATURE_IDENTITY_CREDENTIAL_HARDWARE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_IDENTITY_CREDENTIAL_HARDWARE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_IDENTITY_CREDENTIAL_HARDWARE => + _id_FEATURE_IDENTITY_CREDENTIAL_HARDWARE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_IDENTITY_CREDENTIAL_HARDWARE_DIRECT_ACCESS = _class + .staticFieldId( + r'FEATURE_IDENTITY_CREDENTIAL_HARDWARE_DIRECT_ACCESS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_IDENTITY_CREDENTIAL_HARDWARE_DIRECT_ACCESS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? + get FEATURE_IDENTITY_CREDENTIAL_HARDWARE_DIRECT_ACCESS => + _id_FEATURE_IDENTITY_CREDENTIAL_HARDWARE_DIRECT_ACCESS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_INPUT_METHODS = _class.staticFieldId( + r'FEATURE_INPUT_METHODS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_INPUT_METHODS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_INPUT_METHODS => _id_FEATURE_INPUT_METHODS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_IPSEC_TUNNELS = _class.staticFieldId( + r'FEATURE_IPSEC_TUNNELS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_IPSEC_TUNNELS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_IPSEC_TUNNELS => _id_FEATURE_IPSEC_TUNNELS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_IPSEC_TUNNEL_MIGRATION = _class.staticFieldId( + r'FEATURE_IPSEC_TUNNEL_MIGRATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_IPSEC_TUNNEL_MIGRATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_IPSEC_TUNNEL_MIGRATION => + _id_FEATURE_IPSEC_TUNNEL_MIGRATION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_IRIS = _class.staticFieldId( + r'FEATURE_IRIS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_IRIS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_IRIS => + _id_FEATURE_IRIS.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_KEYSTORE_APP_ATTEST_KEY = _class.staticFieldId( + r'FEATURE_KEYSTORE_APP_ATTEST_KEY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_KEYSTORE_APP_ATTEST_KEY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_KEYSTORE_APP_ATTEST_KEY => + _id_FEATURE_KEYSTORE_APP_ATTEST_KEY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_KEYSTORE_LIMITED_USE_KEY = _class.staticFieldId( + r'FEATURE_KEYSTORE_LIMITED_USE_KEY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_KEYSTORE_LIMITED_USE_KEY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_KEYSTORE_LIMITED_USE_KEY => + _id_FEATURE_KEYSTORE_LIMITED_USE_KEY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_KEYSTORE_SINGLE_USE_KEY = _class.staticFieldId( + r'FEATURE_KEYSTORE_SINGLE_USE_KEY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_KEYSTORE_SINGLE_USE_KEY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_KEYSTORE_SINGLE_USE_KEY => + _id_FEATURE_KEYSTORE_SINGLE_USE_KEY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_LEANBACK = _class.staticFieldId( + r'FEATURE_LEANBACK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_LEANBACK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_LEANBACK => + _id_FEATURE_LEANBACK.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_LEANBACK_ONLY = _class.staticFieldId( + r'FEATURE_LEANBACK_ONLY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_LEANBACK_ONLY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_LEANBACK_ONLY => _id_FEATURE_LEANBACK_ONLY + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_LIVE_TV = _class.staticFieldId( + r'FEATURE_LIVE_TV', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_LIVE_TV` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_LIVE_TV => + _id_FEATURE_LIVE_TV.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_LIVE_WALLPAPER = _class.staticFieldId( + r'FEATURE_LIVE_WALLPAPER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_LIVE_WALLPAPER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_LIVE_WALLPAPER => _id_FEATURE_LIVE_WALLPAPER + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_LOCATION = _class.staticFieldId( + r'FEATURE_LOCATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_LOCATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_LOCATION => + _id_FEATURE_LOCATION.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_LOCATION_GPS = _class.staticFieldId( + r'FEATURE_LOCATION_GPS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_LOCATION_GPS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_LOCATION_GPS => _id_FEATURE_LOCATION_GPS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_LOCATION_NETWORK = _class.staticFieldId( + r'FEATURE_LOCATION_NETWORK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_LOCATION_NETWORK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_LOCATION_NETWORK => + _id_FEATURE_LOCATION_NETWORK.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_MANAGED_USERS = _class.staticFieldId( + r'FEATURE_MANAGED_USERS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_MANAGED_USERS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_MANAGED_USERS => _id_FEATURE_MANAGED_USERS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_MICROPHONE = _class.staticFieldId( + r'FEATURE_MICROPHONE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_MICROPHONE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_MICROPHONE => + _id_FEATURE_MICROPHONE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_MIDI = _class.staticFieldId( + r'FEATURE_MIDI', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_MIDI` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_MIDI => + _id_FEATURE_MIDI.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_NFC = _class.staticFieldId( + r'FEATURE_NFC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_NFC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_NFC => + _id_FEATURE_NFC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_NFC_BEAM = _class.staticFieldId( + r'FEATURE_NFC_BEAM', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_NFC_BEAM` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_NFC_BEAM => + _id_FEATURE_NFC_BEAM.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_NFC_HOST_CARD_EMULATION = _class.staticFieldId( + r'FEATURE_NFC_HOST_CARD_EMULATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_NFC_HOST_CARD_EMULATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_NFC_HOST_CARD_EMULATION => + _id_FEATURE_NFC_HOST_CARD_EMULATION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_NFC_HOST_CARD_EMULATION_NFCF = _class.staticFieldId( + r'FEATURE_NFC_HOST_CARD_EMULATION_NFCF', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_NFC_HOST_CARD_EMULATION_NFCF` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_NFC_HOST_CARD_EMULATION_NFCF => + _id_FEATURE_NFC_HOST_CARD_EMULATION_NFCF.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE = _class + .staticFieldId( + r'FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE => + _id_FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC = _class + .staticFieldId( + r'FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC => + _id_FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_OPENGLES_DEQP_LEVEL = _class.staticFieldId( + r'FEATURE_OPENGLES_DEQP_LEVEL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_OPENGLES_DEQP_LEVEL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_OPENGLES_DEQP_LEVEL => + _id_FEATURE_OPENGLES_DEQP_LEVEL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_OPENGLES_EXTENSION_PACK = _class.staticFieldId( + r'FEATURE_OPENGLES_EXTENSION_PACK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_OPENGLES_EXTENSION_PACK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_OPENGLES_EXTENSION_PACK => + _id_FEATURE_OPENGLES_EXTENSION_PACK.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_PC = _class.staticFieldId( + r'FEATURE_PC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_PC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_PC => + _id_FEATURE_PC.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_PICTURE_IN_PICTURE = _class.staticFieldId( + r'FEATURE_PICTURE_IN_PICTURE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_PICTURE_IN_PICTURE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_PICTURE_IN_PICTURE => + _id_FEATURE_PICTURE_IN_PICTURE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_PRINTING = _class.staticFieldId( + r'FEATURE_PRINTING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_PRINTING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_PRINTING => + _id_FEATURE_PRINTING.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_RAM_LOW = _class.staticFieldId( + r'FEATURE_RAM_LOW', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_RAM_LOW` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_RAM_LOW => + _id_FEATURE_RAM_LOW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_RAM_NORMAL = _class.staticFieldId( + r'FEATURE_RAM_NORMAL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_RAM_NORMAL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_RAM_NORMAL => + _id_FEATURE_RAM_NORMAL.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_SCREEN_LANDSCAPE = _class.staticFieldId( + r'FEATURE_SCREEN_LANDSCAPE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SCREEN_LANDSCAPE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SCREEN_LANDSCAPE => + _id_FEATURE_SCREEN_LANDSCAPE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SCREEN_PORTRAIT = _class.staticFieldId( + r'FEATURE_SCREEN_PORTRAIT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SCREEN_PORTRAIT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SCREEN_PORTRAIT => + _id_FEATURE_SCREEN_PORTRAIT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SECURELY_REMOVES_USERS = _class.staticFieldId( + r'FEATURE_SECURELY_REMOVES_USERS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SECURELY_REMOVES_USERS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SECURELY_REMOVES_USERS => + _id_FEATURE_SECURELY_REMOVES_USERS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SECURE_LOCK_SCREEN = _class.staticFieldId( + r'FEATURE_SECURE_LOCK_SCREEN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SECURE_LOCK_SCREEN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SECURE_LOCK_SCREEN => + _id_FEATURE_SECURE_LOCK_SCREEN.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SECURITY_MODEL_COMPATIBLE = _class.staticFieldId( + r'FEATURE_SECURITY_MODEL_COMPATIBLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SECURITY_MODEL_COMPATIBLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SECURITY_MODEL_COMPATIBLE => + _id_FEATURE_SECURITY_MODEL_COMPATIBLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_ACCELEROMETER = _class.staticFieldId( + r'FEATURE_SENSOR_ACCELEROMETER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_ACCELEROMETER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_ACCELEROMETER => + _id_FEATURE_SENSOR_ACCELEROMETER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES = _class + .staticFieldId( + r'FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES => + _id_FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED = + _class.staticFieldId( + r'FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? + get FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED => + _id_FEATURE_SENSOR_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_AMBIENT_TEMPERATURE = _class.staticFieldId( + r'FEATURE_SENSOR_AMBIENT_TEMPERATURE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_AMBIENT_TEMPERATURE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_AMBIENT_TEMPERATURE => + _id_FEATURE_SENSOR_AMBIENT_TEMPERATURE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_BAROMETER = _class.staticFieldId( + r'FEATURE_SENSOR_BAROMETER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_BAROMETER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_BAROMETER => + _id_FEATURE_SENSOR_BAROMETER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_COMPASS = _class.staticFieldId( + r'FEATURE_SENSOR_COMPASS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_COMPASS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_COMPASS => _id_FEATURE_SENSOR_COMPASS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_SENSOR_DYNAMIC_HEAD_TRACKER = _class.staticFieldId( + r'FEATURE_SENSOR_DYNAMIC_HEAD_TRACKER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_DYNAMIC_HEAD_TRACKER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_DYNAMIC_HEAD_TRACKER => + _id_FEATURE_SENSOR_DYNAMIC_HEAD_TRACKER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_GYROSCOPE = _class.staticFieldId( + r'FEATURE_SENSOR_GYROSCOPE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_GYROSCOPE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_GYROSCOPE => + _id_FEATURE_SENSOR_GYROSCOPE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES = _class.staticFieldId( + r'FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES => + _id_FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES_UNCALIBRATED = _class + .staticFieldId( + r'FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES_UNCALIBRATED', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES_UNCALIBRATED` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? + get FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES_UNCALIBRATED => + _id_FEATURE_SENSOR_GYROSCOPE_LIMITED_AXES_UNCALIBRATED.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_HEADING = _class.staticFieldId( + r'FEATURE_SENSOR_HEADING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_HEADING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_HEADING => _id_FEATURE_SENSOR_HEADING + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_SENSOR_HEART_RATE = _class.staticFieldId( + r'FEATURE_SENSOR_HEART_RATE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_HEART_RATE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_HEART_RATE => + _id_FEATURE_SENSOR_HEART_RATE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_HEART_RATE_ECG = _class.staticFieldId( + r'FEATURE_SENSOR_HEART_RATE_ECG', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_HEART_RATE_ECG` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_HEART_RATE_ECG => + _id_FEATURE_SENSOR_HEART_RATE_ECG.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_HINGE_ANGLE = _class.staticFieldId( + r'FEATURE_SENSOR_HINGE_ANGLE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_HINGE_ANGLE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_HINGE_ANGLE => + _id_FEATURE_SENSOR_HINGE_ANGLE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_LIGHT = _class.staticFieldId( + r'FEATURE_SENSOR_LIGHT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_LIGHT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_LIGHT => _id_FEATURE_SENSOR_LIGHT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_SENSOR_PROXIMITY = _class.staticFieldId( + r'FEATURE_SENSOR_PROXIMITY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_PROXIMITY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_PROXIMITY => + _id_FEATURE_SENSOR_PROXIMITY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_RELATIVE_HUMIDITY = _class.staticFieldId( + r'FEATURE_SENSOR_RELATIVE_HUMIDITY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_RELATIVE_HUMIDITY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_RELATIVE_HUMIDITY => + _id_FEATURE_SENSOR_RELATIVE_HUMIDITY.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_STEP_COUNTER = _class.staticFieldId( + r'FEATURE_SENSOR_STEP_COUNTER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_STEP_COUNTER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_STEP_COUNTER => + _id_FEATURE_SENSOR_STEP_COUNTER.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SENSOR_STEP_DETECTOR = _class.staticFieldId( + r'FEATURE_SENSOR_STEP_DETECTOR', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SENSOR_STEP_DETECTOR` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SENSOR_STEP_DETECTOR => + _id_FEATURE_SENSOR_STEP_DETECTOR.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_SE_OMAPI_ESE = _class.staticFieldId( + r'FEATURE_SE_OMAPI_ESE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SE_OMAPI_ESE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SE_OMAPI_ESE => _id_FEATURE_SE_OMAPI_ESE + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_SE_OMAPI_SD = _class.staticFieldId( + r'FEATURE_SE_OMAPI_SD', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SE_OMAPI_SD` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SE_OMAPI_SD => + _id_FEATURE_SE_OMAPI_SD.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_SE_OMAPI_UICC = _class.staticFieldId( + r'FEATURE_SE_OMAPI_UICC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SE_OMAPI_UICC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SE_OMAPI_UICC => _id_FEATURE_SE_OMAPI_UICC + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_SIP = _class.staticFieldId( + r'FEATURE_SIP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SIP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SIP => + _id_FEATURE_SIP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_SIP_VOIP = _class.staticFieldId( + r'FEATURE_SIP_VOIP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_SIP_VOIP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_SIP_VOIP => + _id_FEATURE_SIP_VOIP.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_STRONGBOX_KEYSTORE = _class.staticFieldId( + r'FEATURE_STRONGBOX_KEYSTORE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_STRONGBOX_KEYSTORE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_STRONGBOX_KEYSTORE => + _id_FEATURE_STRONGBOX_KEYSTORE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_TELECOM = _class.staticFieldId( + r'FEATURE_TELECOM', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELECOM` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELECOM => + _id_FEATURE_TELECOM.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_TELEPHONY = _class.staticFieldId( + r'FEATURE_TELEPHONY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY => + _id_FEATURE_TELEPHONY.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_TELEPHONY_CALLING = _class.staticFieldId( + r'FEATURE_TELEPHONY_CALLING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_CALLING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_CALLING => + _id_FEATURE_TELEPHONY_CALLING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_TELEPHONY_CDMA = _class.staticFieldId( + r'FEATURE_TELEPHONY_CDMA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_CDMA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_CDMA => _id_FEATURE_TELEPHONY_CDMA + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_TELEPHONY_DATA = _class.staticFieldId( + r'FEATURE_TELEPHONY_DATA', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_DATA` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_DATA => _id_FEATURE_TELEPHONY_DATA + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_TELEPHONY_EUICC = _class.staticFieldId( + r'FEATURE_TELEPHONY_EUICC', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_EUICC` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_EUICC => + _id_FEATURE_TELEPHONY_EUICC.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_TELEPHONY_EUICC_MEP = _class.staticFieldId( + r'FEATURE_TELEPHONY_EUICC_MEP', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_EUICC_MEP` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_EUICC_MEP => + _id_FEATURE_TELEPHONY_EUICC_MEP.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_TELEPHONY_GSM = _class.staticFieldId( + r'FEATURE_TELEPHONY_GSM', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_GSM` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_GSM => _id_FEATURE_TELEPHONY_GSM + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_TELEPHONY_IMS = _class.staticFieldId( + r'FEATURE_TELEPHONY_IMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_IMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_IMS => _id_FEATURE_TELEPHONY_IMS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_TELEPHONY_MBMS = _class.staticFieldId( + r'FEATURE_TELEPHONY_MBMS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_MBMS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_MBMS => _id_FEATURE_TELEPHONY_MBMS + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_TELEPHONY_MESSAGING = _class.staticFieldId( + r'FEATURE_TELEPHONY_MESSAGING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_MESSAGING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_MESSAGING => + _id_FEATURE_TELEPHONY_MESSAGING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_TELEPHONY_RADIO_ACCESS = _class.staticFieldId( + r'FEATURE_TELEPHONY_RADIO_ACCESS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_RADIO_ACCESS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_RADIO_ACCESS => + _id_FEATURE_TELEPHONY_RADIO_ACCESS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_TELEPHONY_SUBSCRIPTION = _class.staticFieldId( + r'FEATURE_TELEPHONY_SUBSCRIPTION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEPHONY_SUBSCRIPTION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEPHONY_SUBSCRIPTION => + _id_FEATURE_TELEPHONY_SUBSCRIPTION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_TELEVISION = _class.staticFieldId( + r'FEATURE_TELEVISION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TELEVISION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TELEVISION => + _id_FEATURE_TELEVISION.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_THREAD_NETWORK = _class.staticFieldId( + r'FEATURE_THREAD_NETWORK', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_THREAD_NETWORK` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_THREAD_NETWORK => _id_FEATURE_THREAD_NETWORK + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_TOUCHSCREEN = _class.staticFieldId( + r'FEATURE_TOUCHSCREEN', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TOUCHSCREEN` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TOUCHSCREEN => + _id_FEATURE_TOUCHSCREEN.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_TOUCHSCREEN_MULTITOUCH = _class.staticFieldId( + r'FEATURE_TOUCHSCREEN_MULTITOUCH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TOUCHSCREEN_MULTITOUCH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TOUCHSCREEN_MULTITOUCH => + _id_FEATURE_TOUCHSCREEN_MULTITOUCH.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT = _class + .staticFieldId( + r'FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT => + _id_FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND = _class + .staticFieldId( + r'FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND => + _id_FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_USB_ACCESSORY = _class.staticFieldId( + r'FEATURE_USB_ACCESSORY', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_USB_ACCESSORY` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_USB_ACCESSORY => _id_FEATURE_USB_ACCESSORY + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_USB_HOST = _class.staticFieldId( + r'FEATURE_USB_HOST', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_USB_HOST` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_USB_HOST => + _id_FEATURE_USB_HOST.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_UWB = _class.staticFieldId( + r'FEATURE_UWB', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_UWB` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_UWB => + _id_FEATURE_UWB.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_VERIFIED_BOOT = _class.staticFieldId( + r'FEATURE_VERIFIED_BOOT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_VERIFIED_BOOT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_VERIFIED_BOOT => _id_FEATURE_VERIFIED_BOOT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_VR_HEADTRACKING = _class.staticFieldId( + r'FEATURE_VR_HEADTRACKING', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_VR_HEADTRACKING` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_VR_HEADTRACKING => + _id_FEATURE_VR_HEADTRACKING.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_VR_MODE = _class.staticFieldId( + r'FEATURE_VR_MODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_VR_MODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_VR_MODE => + _id_FEATURE_VR_MODE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_VR_MODE_HIGH_PERFORMANCE = _class.staticFieldId( + r'FEATURE_VR_MODE_HIGH_PERFORMANCE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_VR_MODE_HIGH_PERFORMANCE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_VR_MODE_HIGH_PERFORMANCE => + _id_FEATURE_VR_MODE_HIGH_PERFORMANCE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_VULKAN_DEQP_LEVEL = _class.staticFieldId( + r'FEATURE_VULKAN_DEQP_LEVEL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_VULKAN_DEQP_LEVEL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_VULKAN_DEQP_LEVEL => + _id_FEATURE_VULKAN_DEQP_LEVEL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_VULKAN_HARDWARE_COMPUTE = _class.staticFieldId( + r'FEATURE_VULKAN_HARDWARE_COMPUTE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_VULKAN_HARDWARE_COMPUTE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_VULKAN_HARDWARE_COMPUTE => + _id_FEATURE_VULKAN_HARDWARE_COMPUTE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_VULKAN_HARDWARE_LEVEL = _class.staticFieldId( + r'FEATURE_VULKAN_HARDWARE_LEVEL', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_VULKAN_HARDWARE_LEVEL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_VULKAN_HARDWARE_LEVEL => + _id_FEATURE_VULKAN_HARDWARE_LEVEL.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_VULKAN_HARDWARE_VERSION = _class.staticFieldId( + r'FEATURE_VULKAN_HARDWARE_VERSION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_VULKAN_HARDWARE_VERSION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_VULKAN_HARDWARE_VERSION => + _id_FEATURE_VULKAN_HARDWARE_VERSION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_WALLET_LOCATION_BASED_SUGGESTIONS = _class + .staticFieldId( + r'FEATURE_WALLET_LOCATION_BASED_SUGGESTIONS', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_WALLET_LOCATION_BASED_SUGGESTIONS` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_WALLET_LOCATION_BASED_SUGGESTIONS => + _id_FEATURE_WALLET_LOCATION_BASED_SUGGESTIONS.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_FEATURE_WATCH = _class.staticFieldId( + r'FEATURE_WATCH', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_WATCH` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_WATCH => + _id_FEATURE_WATCH.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_WEBVIEW = _class.staticFieldId( + r'FEATURE_WEBVIEW', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_WEBVIEW` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_WEBVIEW => + _id_FEATURE_WEBVIEW.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_WIFI = _class.staticFieldId( + r'FEATURE_WIFI', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_WIFI` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_WIFI => + _id_FEATURE_WIFI.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_WIFI_AWARE = _class.staticFieldId( + r'FEATURE_WIFI_AWARE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_WIFI_AWARE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_WIFI_AWARE => + _id_FEATURE_WIFI_AWARE.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_WIFI_DIRECT = _class.staticFieldId( + r'FEATURE_WIFI_DIRECT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_WIFI_DIRECT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_WIFI_DIRECT => + _id_FEATURE_WIFI_DIRECT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_WIFI_PASSPOINT = _class.staticFieldId( + r'FEATURE_WIFI_PASSPOINT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_WIFI_PASSPOINT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_WIFI_PASSPOINT => _id_FEATURE_WIFI_PASSPOINT + .get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_WIFI_RTT = _class.staticFieldId( + r'FEATURE_WIFI_RTT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_WIFI_RTT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_WIFI_RTT => + _id_FEATURE_WIFI_RTT.get(_class, const jni$_.$JString$NullableType$()); + + static final _id_FEATURE_WINDOW_MAGNIFICATION = _class.staticFieldId( + r'FEATURE_WINDOW_MAGNIFICATION', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String FEATURE_WINDOW_MAGNIFICATION` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get FEATURE_WINDOW_MAGNIFICATION => + _id_FEATURE_WINDOW_MAGNIFICATION.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + /// from: `static public final int FLAG_PERMISSION_WHITELIST_INSTALLER` + static const FLAG_PERMISSION_WHITELIST_INSTALLER = 2; + + /// from: `static public final int FLAG_PERMISSION_WHITELIST_SYSTEM` + static const FLAG_PERMISSION_WHITELIST_SYSTEM = 1; + + /// from: `static public final int FLAG_PERMISSION_WHITELIST_UPGRADE` + static const FLAG_PERMISSION_WHITELIST_UPGRADE = 4; + + /// from: `static public final int GET_ACTIVITIES` + static const GET_ACTIVITIES = 1; + + /// from: `static public final int GET_ATTRIBUTIONS` + static const GET_ATTRIBUTIONS = -2147483648; + + /// from: `static public final long GET_ATTRIBUTIONS_LONG` + static const GET_ATTRIBUTIONS_LONG = 2147483648; + + /// from: `static public final int GET_CONFIGURATIONS` + static const GET_CONFIGURATIONS = 16384; + + /// from: `static public final int GET_DISABLED_COMPONENTS` + static const GET_DISABLED_COMPONENTS = 512; + + /// from: `static public final int GET_DISABLED_UNTIL_USED_COMPONENTS` + static const GET_DISABLED_UNTIL_USED_COMPONENTS = 32768; + + /// from: `static public final int GET_GIDS` + static const GET_GIDS = 256; + + /// from: `static public final int GET_INSTRUMENTATION` + static const GET_INSTRUMENTATION = 16; + + /// from: `static public final int GET_INTENT_FILTERS` + static const GET_INTENT_FILTERS = 32; + + /// from: `static public final int GET_META_DATA` + static const GET_META_DATA = 128; + + /// from: `static public final int GET_PERMISSIONS` + static const GET_PERMISSIONS = 4096; + + /// from: `static public final int GET_PROVIDERS` + static const GET_PROVIDERS = 8; + + /// from: `static public final int GET_RECEIVERS` + static const GET_RECEIVERS = 2; + + /// from: `static public final int GET_RESOLVED_FILTER` + static const GET_RESOLVED_FILTER = 64; + + /// from: `static public final int GET_SERVICES` + static const GET_SERVICES = 4; + + /// from: `static public final int GET_SHARED_LIBRARY_FILES` + static const GET_SHARED_LIBRARY_FILES = 1024; + + /// from: `static public final int GET_SIGNATURES` + static const GET_SIGNATURES = 64; + + /// from: `static public final int GET_SIGNING_CERTIFICATES` + static const GET_SIGNING_CERTIFICATES = 134217728; + + /// from: `static public final int GET_UNINSTALLED_PACKAGES` + static const GET_UNINSTALLED_PACKAGES = 8192; + + /// from: `static public final int GET_URI_PERMISSION_PATTERNS` + static const GET_URI_PERMISSION_PATTERNS = 2048; + + /// from: `static public final int INSTALL_REASON_DEVICE_RESTORE` + static const INSTALL_REASON_DEVICE_RESTORE = 2; + + /// from: `static public final int INSTALL_REASON_DEVICE_SETUP` + static const INSTALL_REASON_DEVICE_SETUP = 3; + + /// from: `static public final int INSTALL_REASON_POLICY` + static const INSTALL_REASON_POLICY = 1; + + /// from: `static public final int INSTALL_REASON_UNKNOWN` + static const INSTALL_REASON_UNKNOWN = 0; + + /// from: `static public final int INSTALL_REASON_USER` + static const INSTALL_REASON_USER = 4; + + /// from: `static public final int INSTALL_SCENARIO_BULK` + static const INSTALL_SCENARIO_BULK = 2; + + /// from: `static public final int INSTALL_SCENARIO_BULK_SECONDARY` + static const INSTALL_SCENARIO_BULK_SECONDARY = 3; + + /// from: `static public final int INSTALL_SCENARIO_DEFAULT` + static const INSTALL_SCENARIO_DEFAULT = 0; + + /// from: `static public final int INSTALL_SCENARIO_FAST` + static const INSTALL_SCENARIO_FAST = 1; + + /// from: `static public final int MATCH_ALL` + static const MATCH_ALL = 131072; + + /// from: `static public final int MATCH_APEX` + static const MATCH_APEX = 1073741824; + + /// from: `static public final long MATCH_ARCHIVED_PACKAGES` + static const MATCH_ARCHIVED_PACKAGES = 4294967296; + + /// from: `static public final int MATCH_DEFAULT_ONLY` + static const MATCH_DEFAULT_ONLY = 65536; + + /// from: `static public final int MATCH_DIRECT_BOOT_AUTO` + static const MATCH_DIRECT_BOOT_AUTO = 268435456; + + /// from: `static public final int MATCH_DIRECT_BOOT_AWARE` + static const MATCH_DIRECT_BOOT_AWARE = 524288; + + /// from: `static public final int MATCH_DIRECT_BOOT_UNAWARE` + static const MATCH_DIRECT_BOOT_UNAWARE = 262144; + + /// from: `static public final int MATCH_DISABLED_COMPONENTS` + static const MATCH_DISABLED_COMPONENTS = 512; + + /// from: `static public final int MATCH_DISABLED_UNTIL_USED_COMPONENTS` + static const MATCH_DISABLED_UNTIL_USED_COMPONENTS = 32768; + + /// from: `static public final int MATCH_SYSTEM_ONLY` + static const MATCH_SYSTEM_ONLY = 1048576; + + /// from: `static public final int MATCH_UNINSTALLED_PACKAGES` + static const MATCH_UNINSTALLED_PACKAGES = 8192; + + /// from: `static public final long MAXIMUM_VERIFICATION_TIMEOUT` + static const MAXIMUM_VERIFICATION_TIMEOUT = 3600000; + + /// from: `static public final int PERMISSION_DENIED` + static const PERMISSION_DENIED = -1; + + /// from: `static public final int PERMISSION_GRANTED` + static const PERMISSION_GRANTED = 0; + static final _id_PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT = _class + .staticFieldId( + r'PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT => + _id_PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_PROPERTY_MEDIA_CAPABILITIES = _class.staticFieldId( + r'PROPERTY_MEDIA_CAPABILITIES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PROPERTY_MEDIA_CAPABILITIES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PROPERTY_MEDIA_CAPABILITIES => + _id_PROPERTY_MEDIA_CAPABILITIES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_PROPERTY_SELF_CERTIFIED_NETWORK_CAPABILITIES = _class + .staticFieldId( + r'PROPERTY_SELF_CERTIFIED_NETWORK_CAPABILITIES', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PROPERTY_SELF_CERTIFIED_NETWORK_CAPABILITIES` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PROPERTY_SELF_CERTIFIED_NETWORK_CAPABILITIES => + _id_PROPERTY_SELF_CERTIFIED_NETWORK_CAPABILITIES.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_PROPERTY_SPECIAL_USE_FGS_SUBTYPE = _class.staticFieldId( + r'PROPERTY_SPECIAL_USE_FGS_SUBTYPE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PROPERTY_SPECIAL_USE_FGS_SUBTYPE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PROPERTY_SPECIAL_USE_FGS_SUBTYPE => + _id_PROPERTY_SPECIAL_USE_FGS_SUBTYPE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + static final _id_PROPERTY_USE_RESTRICTED_BACKUP_MODE = _class.staticFieldId( + r'PROPERTY_USE_RESTRICTED_BACKUP_MODE', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String PROPERTY_USE_RESTRICTED_BACKUP_MODE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get PROPERTY_USE_RESTRICTED_BACKUP_MODE => + _id_PROPERTY_USE_RESTRICTED_BACKUP_MODE.get( + _class, + const jni$_.$JString$NullableType$(), + ); + + /// from: `static public final int SIGNATURE_FIRST_NOT_SIGNED` + static const SIGNATURE_FIRST_NOT_SIGNED = -1; + + /// from: `static public final int SIGNATURE_MATCH` + static const SIGNATURE_MATCH = 0; + + /// from: `static public final int SIGNATURE_NEITHER_SIGNED` + static const SIGNATURE_NEITHER_SIGNED = 1; + + /// from: `static public final int SIGNATURE_NO_MATCH` + static const SIGNATURE_NO_MATCH = -3; + + /// from: `static public final int SIGNATURE_SECOND_NOT_SIGNED` + static const SIGNATURE_SECOND_NOT_SIGNED = -2; + + /// from: `static public final int SIGNATURE_UNKNOWN_PACKAGE` + static const SIGNATURE_UNKNOWN_PACKAGE = -4; + + /// from: `static public final int SYNCHRONOUS` + static const SYNCHRONOUS = 2; + static final _id_TRUST_ALL = _class.staticFieldId( + r'TRUST_ALL', + r'Ljava/util/List;', + ); + + /// from: `static public final java.util.List TRUST_ALL` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JList? get TRUST_ALL => _id_TRUST_ALL.get( + _class, + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + + static final _id_TRUST_NONE = _class.staticFieldId( + r'TRUST_NONE', + r'Ljava/util/List;', + ); + + /// from: `static public final java.util.List TRUST_NONE` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JList? get TRUST_NONE => _id_TRUST_NONE.get( + _class, + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + + /// from: `static public final int VERIFICATION_ALLOW` + static const VERIFICATION_ALLOW = 1; + + /// from: `static public final int VERIFICATION_REJECT` + static const VERIFICATION_REJECT = -1; + + /// from: `static public final int VERSION_CODE_HIGHEST` + static const VERSION_CODE_HIGHEST = -1; + static final _id_addPackageToPreferred = _class.instanceMethodId( + r'addPackageToPreferred', + r'(Ljava/lang/String;)V', + ); + + static final _addPackageToPreferred = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void addPackageToPreferred(java.lang.String string)` + void addPackageToPreferred(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + _addPackageToPreferred( + reference.pointer, + _id_addPackageToPreferred as jni$_.JMethodIDPtr, + _$string.pointer, + ).check(); + } + + static final _id_addPermission = _class.instanceMethodId( + r'addPermission', + r'(Landroid/content/pm/PermissionInfo;)Z', + ); + + static final _addPermission = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract boolean addPermission(android.content.pm.PermissionInfo permissionInfo)` + bool addPermission(jni$_.JObject? permissionInfo) { + final _$permissionInfo = permissionInfo?.reference ?? jni$_.jNullReference; + return _addPermission( + reference.pointer, + _id_addPermission as jni$_.JMethodIDPtr, + _$permissionInfo.pointer, + ).boolean; + } + + static final _id_addPermissionAsync = _class.instanceMethodId( + r'addPermissionAsync', + r'(Landroid/content/pm/PermissionInfo;)Z', + ); + + static final _addPermissionAsync = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract boolean addPermissionAsync(android.content.pm.PermissionInfo permissionInfo)` + bool addPermissionAsync(jni$_.JObject? permissionInfo) { + final _$permissionInfo = permissionInfo?.reference ?? jni$_.jNullReference; + return _addPermissionAsync( + reference.pointer, + _id_addPermissionAsync as jni$_.JMethodIDPtr, + _$permissionInfo.pointer, + ).boolean; + } + + static final _id_addPreferredActivity = _class.instanceMethodId( + r'addPreferredActivity', + r'(Landroid/content/IntentFilter;I[Landroid/content/ComponentName;Landroid/content/ComponentName;)V', + ); + + static final _addPreferredActivity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void addPreferredActivity(android.content.IntentFilter intentFilter, int i, android.content.ComponentName[] componentNames, android.content.ComponentName componentName)` + void addPreferredActivity( + jni$_.JObject? intentFilter, + int i, + jni$_.JArray? componentNames, + jni$_.JObject? componentName, + ) { + final _$intentFilter = intentFilter?.reference ?? jni$_.jNullReference; + final _$componentNames = componentNames?.reference ?? jni$_.jNullReference; + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + _addPreferredActivity( + reference.pointer, + _id_addPreferredActivity as jni$_.JMethodIDPtr, + _$intentFilter.pointer, + i, + _$componentNames.pointer, + _$componentName.pointer, + ).check(); + } + + static final _id_addWhitelistedRestrictedPermission = _class.instanceMethodId( + r'addWhitelistedRestrictedPermission', + r'(Ljava/lang/String;Ljava/lang/String;I)Z', + ); + + static final _addWhitelistedRestrictedPermission = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public boolean addWhitelistedRestrictedPermission(java.lang.String string, java.lang.String string1, int i)` + bool addWhitelistedRestrictedPermission( + jni$_.JString? string, + jni$_.JString? string1, + int i, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _addWhitelistedRestrictedPermission( + reference.pointer, + _id_addWhitelistedRestrictedPermission as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + i, + ).boolean; + } + + static final _id_canPackageQuery = _class.instanceMethodId( + r'canPackageQuery', + r'(Ljava/lang/String;Ljava/lang/String;)Z', + ); + + static final _canPackageQuery = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean canPackageQuery(java.lang.String string, java.lang.String string1)` + bool canPackageQuery(jni$_.JString? string, jni$_.JString? string1) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _canPackageQuery( + reference.pointer, + _id_canPackageQuery as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + ).boolean; + } + + static final _id_canPackageQuery$1 = _class.instanceMethodId( + r'canPackageQuery', + r'(Ljava/lang/String;[Ljava/lang/String;)[Z', + ); + + static final _canPackageQuery$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean[] canPackageQuery(java.lang.String string, java.lang.String[] strings)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JBooleanArray? canPackageQuery$1( + jni$_.JString? string, + jni$_.JArray? strings, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$strings = strings?.reference ?? jni$_.jNullReference; + return _canPackageQuery$1( + reference.pointer, + _id_canPackageQuery$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$strings.pointer, + ).object(const jni$_.$JBooleanArray$NullableType$()); + } + + static final _id_canRequestPackageInstalls = _class.instanceMethodId( + r'canRequestPackageInstalls', + r'()Z', + ); + + static final _canRequestPackageInstalls = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract boolean canRequestPackageInstalls()` + bool canRequestPackageInstalls() { + return _canRequestPackageInstalls( + reference.pointer, + _id_canRequestPackageInstalls as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_canonicalToCurrentPackageNames = _class.instanceMethodId( + r'canonicalToCurrentPackageNames', + r'([Ljava/lang/String;)[Ljava/lang/String;', + ); + + static final _canonicalToCurrentPackageNames = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract java.lang.String[] canonicalToCurrentPackageNames(java.lang.String[] strings)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? canonicalToCurrentPackageNames( + jni$_.JArray? strings, + ) { + final _$strings = strings?.reference ?? jni$_.jNullReference; + return _canonicalToCurrentPackageNames( + reference.pointer, + _id_canonicalToCurrentPackageNames as jni$_.JMethodIDPtr, + _$strings.pointer, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JString$NullableType$(), + ), + ); + } + + static final _id_checkPermission = _class.instanceMethodId( + r'checkPermission', + r'(Ljava/lang/String;Ljava/lang/String;)I', + ); + + static final _checkPermission = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract int checkPermission(java.lang.String string, java.lang.String string1)` + int checkPermission(jni$_.JString? string, jni$_.JString? string1) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _checkPermission( + reference.pointer, + _id_checkPermission as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + ).integer; + } + + static final _id_checkSignatures = _class.instanceMethodId( + r'checkSignatures', + r'(II)I', + ); + + static final _checkSignatures = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Int32)>, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + int, + ) + >(); + + /// from: `public abstract int checkSignatures(int i, int i1)` + int checkSignatures(int i, int i1) { + return _checkSignatures( + reference.pointer, + _id_checkSignatures as jni$_.JMethodIDPtr, + i, + i1, + ).integer; + } + + static final _id_checkSignatures$1 = _class.instanceMethodId( + r'checkSignatures', + r'(Ljava/lang/String;Ljava/lang/String;)I', + ); + + static final _checkSignatures$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract int checkSignatures(java.lang.String string, java.lang.String string1)` + int checkSignatures$1(jni$_.JString? string, jni$_.JString? string1) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _checkSignatures$1( + reference.pointer, + _id_checkSignatures$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + ).integer; + } + + static final _id_clearInstantAppCookie = _class.instanceMethodId( + r'clearInstantAppCookie', + r'()V', + ); + + static final _clearInstantAppCookie = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract void clearInstantAppCookie()` + void clearInstantAppCookie() { + _clearInstantAppCookie( + reference.pointer, + _id_clearInstantAppCookie as jni$_.JMethodIDPtr, + ).check(); + } + + static final _id_clearPackagePreferredActivities = _class.instanceMethodId( + r'clearPackagePreferredActivities', + r'(Ljava/lang/String;)V', + ); + + static final _clearPackagePreferredActivities = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void clearPackagePreferredActivities(java.lang.String string)` + void clearPackagePreferredActivities(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + _clearPackagePreferredActivities( + reference.pointer, + _id_clearPackagePreferredActivities as jni$_.JMethodIDPtr, + _$string.pointer, + ).check(); + } + + static final _id_currentToCanonicalPackageNames = _class.instanceMethodId( + r'currentToCanonicalPackageNames', + r'([Ljava/lang/String;)[Ljava/lang/String;', + ); + + static final _currentToCanonicalPackageNames = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract java.lang.String[] currentToCanonicalPackageNames(java.lang.String[] strings)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? currentToCanonicalPackageNames( + jni$_.JArray? strings, + ) { + final _$strings = strings?.reference ?? jni$_.jNullReference; + return _currentToCanonicalPackageNames( + reference.pointer, + _id_currentToCanonicalPackageNames as jni$_.JMethodIDPtr, + _$strings.pointer, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JString$NullableType$(), + ), + ); + } + + static final _id_extendVerificationTimeout = _class.instanceMethodId( + r'extendVerificationTimeout', + r'(IIJ)V', + ); + + static final _extendVerificationTimeout = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Int32, jni$_.Int64)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + int, + int, + ) + >(); + + /// from: `public abstract void extendVerificationTimeout(int i, int i1, long j)` + void extendVerificationTimeout(int i, int i1, int j) { + _extendVerificationTimeout( + reference.pointer, + _id_extendVerificationTimeout as jni$_.JMethodIDPtr, + i, + i1, + j, + ).check(); + } + + static final _id_getActivityBanner = _class.instanceMethodId( + r'getActivityBanner', + r'(Landroid/content/ComponentName;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getActivityBanner = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getActivityBanner(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getActivityBanner(jni$_.JObject? componentName) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getActivityBanner( + reference.pointer, + _id_getActivityBanner as jni$_.JMethodIDPtr, + _$componentName.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getActivityBanner$1 = _class.instanceMethodId( + r'getActivityBanner', + r'(Landroid/content/Intent;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getActivityBanner$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getActivityBanner(android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getActivityBanner$1(Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _getActivityBanner$1( + reference.pointer, + _id_getActivityBanner$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getActivityIcon = _class.instanceMethodId( + r'getActivityIcon', + r'(Landroid/content/ComponentName;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getActivityIcon = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getActivityIcon(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getActivityIcon(jni$_.JObject? componentName) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getActivityIcon( + reference.pointer, + _id_getActivityIcon as jni$_.JMethodIDPtr, + _$componentName.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getActivityIcon$1 = _class.instanceMethodId( + r'getActivityIcon', + r'(Landroid/content/Intent;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getActivityIcon$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getActivityIcon(android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getActivityIcon$1(Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _getActivityIcon$1( + reference.pointer, + _id_getActivityIcon$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getActivityInfo = _class.instanceMethodId( + r'getActivityInfo', + r'(Landroid/content/ComponentName;Landroid/content/pm/PackageManager$ComponentInfoFlags;)Landroid/content/pm/ActivityInfo;', + ); + + static final _getActivityInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.ActivityInfo getActivityInfo(android.content.ComponentName componentName, android.content.pm.PackageManager$ComponentInfoFlags componentInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getActivityInfo( + jni$_.JObject? componentName, + PackageManager$ComponentInfoFlags? componentInfoFlags, + ) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + final _$componentInfoFlags = + componentInfoFlags?.reference ?? jni$_.jNullReference; + return _getActivityInfo( + reference.pointer, + _id_getActivityInfo as jni$_.JMethodIDPtr, + _$componentName.pointer, + _$componentInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getActivityInfo$1 = _class.instanceMethodId( + r'getActivityInfo', + r'(Landroid/content/ComponentName;I)Landroid/content/pm/ActivityInfo;', + ); + + static final _getActivityInfo$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.ActivityInfo getActivityInfo(android.content.ComponentName componentName, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getActivityInfo$1(jni$_.JObject? componentName, int i) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getActivityInfo$1( + reference.pointer, + _id_getActivityInfo$1 as jni$_.JMethodIDPtr, + _$componentName.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getActivityLogo = _class.instanceMethodId( + r'getActivityLogo', + r'(Landroid/content/ComponentName;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getActivityLogo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getActivityLogo(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getActivityLogo(jni$_.JObject? componentName) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getActivityLogo( + reference.pointer, + _id_getActivityLogo as jni$_.JMethodIDPtr, + _$componentName.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getActivityLogo$1 = _class.instanceMethodId( + r'getActivityLogo', + r'(Landroid/content/Intent;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getActivityLogo$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getActivityLogo(android.content.Intent intent)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getActivityLogo$1(Intent? intent) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _getActivityLogo$1( + reference.pointer, + _id_getActivityLogo$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getAllPermissionGroups = _class.instanceMethodId( + r'getAllPermissionGroups', + r'(I)Ljava/util/List;', + ); + + static final _getAllPermissionGroups = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public abstract java.util.List getAllPermissionGroups(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getAllPermissionGroups(int i) { + return _getAllPermissionGroups( + reference.pointer, + _id_getAllPermissionGroups as jni$_.JMethodIDPtr, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getApplicationBanner = _class.instanceMethodId( + r'getApplicationBanner', + r'(Landroid/content/pm/ApplicationInfo;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getApplicationBanner = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getApplicationBanner(android.content.pm.ApplicationInfo applicationInfo)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationBanner(jni$_.JObject? applicationInfo) { + final _$applicationInfo = + applicationInfo?.reference ?? jni$_.jNullReference; + return _getApplicationBanner( + reference.pointer, + _id_getApplicationBanner as jni$_.JMethodIDPtr, + _$applicationInfo.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getApplicationBanner$1 = _class.instanceMethodId( + r'getApplicationBanner', + r'(Ljava/lang/String;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getApplicationBanner$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getApplicationBanner(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationBanner$1(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getApplicationBanner$1( + reference.pointer, + _id_getApplicationBanner$1 as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getApplicationEnabledSetting = _class.instanceMethodId( + r'getApplicationEnabledSetting', + r'(Ljava/lang/String;)I', + ); + + static final _getApplicationEnabledSetting = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract int getApplicationEnabledSetting(java.lang.String string)` + int getApplicationEnabledSetting(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getApplicationEnabledSetting( + reference.pointer, + _id_getApplicationEnabledSetting as jni$_.JMethodIDPtr, + _$string.pointer, + ).integer; + } + + static final _id_getApplicationIcon = _class.instanceMethodId( + r'getApplicationIcon', + r'(Landroid/content/pm/ApplicationInfo;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getApplicationIcon = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getApplicationIcon(android.content.pm.ApplicationInfo applicationInfo)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationIcon(jni$_.JObject? applicationInfo) { + final _$applicationInfo = + applicationInfo?.reference ?? jni$_.jNullReference; + return _getApplicationIcon( + reference.pointer, + _id_getApplicationIcon as jni$_.JMethodIDPtr, + _$applicationInfo.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getApplicationIcon$1 = _class.instanceMethodId( + r'getApplicationIcon', + r'(Ljava/lang/String;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getApplicationIcon$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getApplicationIcon(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationIcon$1(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getApplicationIcon$1( + reference.pointer, + _id_getApplicationIcon$1 as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getApplicationInfo = _class.instanceMethodId( + r'getApplicationInfo', + r'(Ljava/lang/String;Landroid/content/pm/PackageManager$ApplicationInfoFlags;)Landroid/content/pm/ApplicationInfo;', + ); + + static final _getApplicationInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String string, android.content.pm.PackageManager$ApplicationInfoFlags applicationInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationInfo( + jni$_.JString? string, + PackageManager$ApplicationInfoFlags? applicationInfoFlags, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$applicationInfoFlags = + applicationInfoFlags?.reference ?? jni$_.jNullReference; + return _getApplicationInfo( + reference.pointer, + _id_getApplicationInfo as jni$_.JMethodIDPtr, + _$string.pointer, + _$applicationInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getApplicationInfo$1 = _class.instanceMethodId( + r'getApplicationInfo', + r'(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;', + ); + + static final _getApplicationInfo$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationInfo$1(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getApplicationInfo$1( + reference.pointer, + _id_getApplicationInfo$1 as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getApplicationLabel = _class.instanceMethodId( + r'getApplicationLabel', + r'(Landroid/content/pm/ApplicationInfo;)Ljava/lang/CharSequence;', + ); + + static final _getApplicationLabel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo applicationInfo)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationLabel(jni$_.JObject? applicationInfo) { + final _$applicationInfo = + applicationInfo?.reference ?? jni$_.jNullReference; + return _getApplicationLabel( + reference.pointer, + _id_getApplicationLabel as jni$_.JMethodIDPtr, + _$applicationInfo.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getApplicationLogo = _class.instanceMethodId( + r'getApplicationLogo', + r'(Landroid/content/pm/ApplicationInfo;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getApplicationLogo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo applicationInfo)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationLogo(jni$_.JObject? applicationInfo) { + final _$applicationInfo = + applicationInfo?.reference ?? jni$_.jNullReference; + return _getApplicationLogo( + reference.pointer, + _id_getApplicationLogo as jni$_.JMethodIDPtr, + _$applicationInfo.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getApplicationLogo$1 = _class.instanceMethodId( + r'getApplicationLogo', + r'(Ljava/lang/String;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getApplicationLogo$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getApplicationLogo(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getApplicationLogo$1(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getApplicationLogo$1( + reference.pointer, + _id_getApplicationLogo$1 as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getArchivedPackage = _class.instanceMethodId( + r'getArchivedPackage', + r'(Ljava/lang/String;)Landroid/content/pm/ArchivedPackageInfo;', + ); + + static final _getArchivedPackage = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.ArchivedPackageInfo getArchivedPackage(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getArchivedPackage(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getArchivedPackage( + reference.pointer, + _id_getArchivedPackage as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getBackgroundPermissionOptionLabel = _class.instanceMethodId( + r'getBackgroundPermissionOptionLabel', + r'()Ljava/lang/CharSequence;', + ); + + static final _getBackgroundPermissionOptionLabel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public java.lang.CharSequence getBackgroundPermissionOptionLabel()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getBackgroundPermissionOptionLabel() { + return _getBackgroundPermissionOptionLabel( + reference.pointer, + _id_getBackgroundPermissionOptionLabel as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getChangedPackages = _class.instanceMethodId( + r'getChangedPackages', + r'(I)Landroid/content/pm/ChangedPackages;', + ); + + static final _getChangedPackages = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public abstract android.content.pm.ChangedPackages getChangedPackages(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getChangedPackages(int i) { + return _getChangedPackages( + reference.pointer, + _id_getChangedPackages as jni$_.JMethodIDPtr, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getComponentEnabledSetting = _class.instanceMethodId( + r'getComponentEnabledSetting', + r'(Landroid/content/ComponentName;)I', + ); + + static final _getComponentEnabledSetting = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract int getComponentEnabledSetting(android.content.ComponentName componentName)` + int getComponentEnabledSetting(jni$_.JObject? componentName) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getComponentEnabledSetting( + reference.pointer, + _id_getComponentEnabledSetting as jni$_.JMethodIDPtr, + _$componentName.pointer, + ).integer; + } + + static final _id_getDefaultActivityIcon = _class.instanceMethodId( + r'getDefaultActivityIcon', + r'()Landroid/graphics/drawable/Drawable;', + ); + + static final _getDefaultActivityIcon = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getDefaultActivityIcon()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getDefaultActivityIcon() { + return _getDefaultActivityIcon( + reference.pointer, + _id_getDefaultActivityIcon as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getDrawable = _class.instanceMethodId( + r'getDrawable', + r'(Ljava/lang/String;ILandroid/content/pm/ApplicationInfo;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getDrawable = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getDrawable(java.lang.String string, int i, android.content.pm.ApplicationInfo applicationInfo)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getDrawable( + jni$_.JString? string, + int i, + jni$_.JObject? applicationInfo, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$applicationInfo = + applicationInfo?.reference ?? jni$_.jNullReference; + return _getDrawable( + reference.pointer, + _id_getDrawable as jni$_.JMethodIDPtr, + _$string.pointer, + i, + _$applicationInfo.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getGroupOfPlatformPermission = _class.instanceMethodId( + r'getGroupOfPlatformPermission', + r'(Ljava/lang/String;Ljava/util/concurrent/Executor;Ljava/util/function/Consumer;)V', + ); + + static final _getGroupOfPlatformPermission = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void getGroupOfPlatformPermission(java.lang.String string, java.util.concurrent.Executor executor, java.util.function.Consumer consumer)` + void getGroupOfPlatformPermission( + jni$_.JString? string, + jni$_.JObject? executor, + jni$_.JObject? consumer, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$executor = executor?.reference ?? jni$_.jNullReference; + final _$consumer = consumer?.reference ?? jni$_.jNullReference; + _getGroupOfPlatformPermission( + reference.pointer, + _id_getGroupOfPlatformPermission as jni$_.JMethodIDPtr, + _$string.pointer, + _$executor.pointer, + _$consumer.pointer, + ).check(); + } + + static final _id_getInstallSourceInfo = _class.instanceMethodId( + r'getInstallSourceInfo', + r'(Ljava/lang/String;)Landroid/content/pm/InstallSourceInfo;', + ); + + static final _getInstallSourceInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.InstallSourceInfo getInstallSourceInfo(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getInstallSourceInfo(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getInstallSourceInfo( + reference.pointer, + _id_getInstallSourceInfo as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getInstalledApplications = _class.instanceMethodId( + r'getInstalledApplications', + r'(Landroid/content/pm/PackageManager$ApplicationInfoFlags;)Ljava/util/List;', + ); + + static final _getInstalledApplications = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List getInstalledApplications(android.content.pm.PackageManager$ApplicationInfoFlags applicationInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getInstalledApplications( + PackageManager$ApplicationInfoFlags? applicationInfoFlags, + ) { + final _$applicationInfoFlags = + applicationInfoFlags?.reference ?? jni$_.jNullReference; + return _getInstalledApplications( + reference.pointer, + _id_getInstalledApplications as jni$_.JMethodIDPtr, + _$applicationInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getInstalledApplications$1 = _class.instanceMethodId( + r'getInstalledApplications', + r'(I)Ljava/util/List;', + ); + + static final _getInstalledApplications$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public abstract java.util.List getInstalledApplications(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getInstalledApplications$1(int i) { + return _getInstalledApplications$1( + reference.pointer, + _id_getInstalledApplications$1 as jni$_.JMethodIDPtr, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getInstalledModules = _class.instanceMethodId( + r'getInstalledModules', + r'(I)Ljava/util/List;', + ); + + static final _getInstalledModules = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public java.util.List getInstalledModules(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getInstalledModules(int i) { + return _getInstalledModules( + reference.pointer, + _id_getInstalledModules as jni$_.JMethodIDPtr, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getInstalledPackages = _class.instanceMethodId( + r'getInstalledPackages', + r'(Landroid/content/pm/PackageManager$PackageInfoFlags;)Ljava/util/List;', + ); + + static final _getInstalledPackages = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List getInstalledPackages(android.content.pm.PackageManager$PackageInfoFlags packageInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getInstalledPackages( + PackageManager$PackageInfoFlags? packageInfoFlags, + ) { + final _$packageInfoFlags = + packageInfoFlags?.reference ?? jni$_.jNullReference; + return _getInstalledPackages( + reference.pointer, + _id_getInstalledPackages as jni$_.JMethodIDPtr, + _$packageInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getInstalledPackages$1 = _class.instanceMethodId( + r'getInstalledPackages', + r'(I)Ljava/util/List;', + ); + + static final _getInstalledPackages$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public abstract java.util.List getInstalledPackages(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getInstalledPackages$1(int i) { + return _getInstalledPackages$1( + reference.pointer, + _id_getInstalledPackages$1 as jni$_.JMethodIDPtr, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getInstallerPackageName = _class.instanceMethodId( + r'getInstallerPackageName', + r'(Ljava/lang/String;)Ljava/lang/String;', + ); + + static final _getInstallerPackageName = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract java.lang.String getInstallerPackageName(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getInstallerPackageName(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getInstallerPackageName( + reference.pointer, + _id_getInstallerPackageName as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getInstantAppCookie = _class.instanceMethodId( + r'getInstantAppCookie', + r'()[B', + ); + + static final _getInstantAppCookie = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract byte[] getInstantAppCookie()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JByteArray? getInstantAppCookie() { + return _getInstantAppCookie( + reference.pointer, + _id_getInstantAppCookie as jni$_.JMethodIDPtr, + ).object(const jni$_.$JByteArray$NullableType$()); + } + + static final _id_getInstantAppCookieMaxBytes = _class.instanceMethodId( + r'getInstantAppCookieMaxBytes', + r'()I', + ); + + static final _getInstantAppCookieMaxBytes = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract int getInstantAppCookieMaxBytes()` + int getInstantAppCookieMaxBytes() { + return _getInstantAppCookieMaxBytes( + reference.pointer, + _id_getInstantAppCookieMaxBytes as jni$_.JMethodIDPtr, + ).integer; + } + + static final _id_getInstrumentationInfo = _class.instanceMethodId( + r'getInstrumentationInfo', + r'(Landroid/content/ComponentName;I)Landroid/content/pm/InstrumentationInfo;', + ); + + static final _getInstrumentationInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.InstrumentationInfo getInstrumentationInfo(android.content.ComponentName componentName, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getInstrumentationInfo(jni$_.JObject? componentName, int i) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getInstrumentationInfo( + reference.pointer, + _id_getInstrumentationInfo as jni$_.JMethodIDPtr, + _$componentName.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getLaunchIntentForPackage = _class.instanceMethodId( + r'getLaunchIntentForPackage', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _getLaunchIntentForPackage = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.content.Intent getLaunchIntentForPackage(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? getLaunchIntentForPackage(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getLaunchIntentForPackage( + reference.pointer, + _id_getLaunchIntentForPackage as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_getLaunchIntentSenderForPackage = _class.instanceMethodId( + r'getLaunchIntentSenderForPackage', + r'(Ljava/lang/String;)Landroid/content/IntentSender;', + ); + + static final _getLaunchIntentSenderForPackage = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.IntentSender getLaunchIntentSenderForPackage(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getLaunchIntentSenderForPackage(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getLaunchIntentSenderForPackage( + reference.pointer, + _id_getLaunchIntentSenderForPackage as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getLeanbackLaunchIntentForPackage = _class.instanceMethodId( + r'getLeanbackLaunchIntentForPackage', + r'(Ljava/lang/String;)Landroid/content/Intent;', + ); + + static final _getLeanbackLaunchIntentForPackage = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.content.Intent getLeanbackLaunchIntentForPackage(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + Intent? getLeanbackLaunchIntentForPackage(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getLeanbackLaunchIntentForPackage( + reference.pointer, + _id_getLeanbackLaunchIntentForPackage as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const $Intent$NullableType$()); + } + + static final _id_getMimeGroup = _class.instanceMethodId( + r'getMimeGroup', + r'(Ljava/lang/String;)Ljava/util/Set;', + ); + + static final _getMimeGroup = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.Set getMimeGroup(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JSet? getMimeGroup(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getMimeGroup( + reference.pointer, + _id_getMimeGroup as jni$_.JMethodIDPtr, + _$string.pointer, + ).object?>( + const jni$_.$JSet$NullableType$( + jni$_.$JString$NullableType$(), + ), + ); + } + + static final _id_getModuleInfo = _class.instanceMethodId( + r'getModuleInfo', + r'(Ljava/lang/String;I)Landroid/content/pm/ModuleInfo;', + ); + + static final _getModuleInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public android.content.pm.ModuleInfo getModuleInfo(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getModuleInfo(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getModuleInfo( + reference.pointer, + _id_getModuleInfo as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getNameForUid = _class.instanceMethodId( + r'getNameForUid', + r'(I)Ljava/lang/String;', + ); + + static final _getNameForUid = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public abstract java.lang.String getNameForUid(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? getNameForUid(int i) { + return _getNameForUid( + reference.pointer, + _id_getNameForUid as jni$_.JMethodIDPtr, + i, + ).object(const jni$_.$JString$NullableType$()); + } + + static final _id_getPackageArchiveInfo = _class.instanceMethodId( + r'getPackageArchiveInfo', + r'(Ljava/lang/String;Landroid/content/pm/PackageManager$PackageInfoFlags;)Landroid/content/pm/PackageInfo;', + ); + + static final _getPackageArchiveInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.PackageInfo getPackageArchiveInfo(java.lang.String string, android.content.pm.PackageManager$PackageInfoFlags packageInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPackageArchiveInfo( + jni$_.JString? string, + PackageManager$PackageInfoFlags? packageInfoFlags, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$packageInfoFlags = + packageInfoFlags?.reference ?? jni$_.jNullReference; + return _getPackageArchiveInfo( + reference.pointer, + _id_getPackageArchiveInfo as jni$_.JMethodIDPtr, + _$string.pointer, + _$packageInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPackageArchiveInfo$1 = _class.instanceMethodId( + r'getPackageArchiveInfo', + r'(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;', + ); + + static final _getPackageArchiveInfo$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public android.content.pm.PackageInfo getPackageArchiveInfo(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPackageArchiveInfo$1(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getPackageArchiveInfo$1( + reference.pointer, + _id_getPackageArchiveInfo$1 as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPackageGids = _class.instanceMethodId( + r'getPackageGids', + r'(Ljava/lang/String;)[I', + ); + + static final _getPackageGids = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract int[] getPackageGids(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JIntArray? getPackageGids(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getPackageGids( + reference.pointer, + _id_getPackageGids as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JIntArray$NullableType$()); + } + + static final _id_getPackageGids$1 = _class.instanceMethodId( + r'getPackageGids', + r'(Ljava/lang/String;Landroid/content/pm/PackageManager$PackageInfoFlags;)[I', + ); + + static final _getPackageGids$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public int[] getPackageGids(java.lang.String string, android.content.pm.PackageManager$PackageInfoFlags packageInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JIntArray? getPackageGids$1( + jni$_.JString? string, + PackageManager$PackageInfoFlags? packageInfoFlags, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$packageInfoFlags = + packageInfoFlags?.reference ?? jni$_.jNullReference; + return _getPackageGids$1( + reference.pointer, + _id_getPackageGids$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$packageInfoFlags.pointer, + ).object(const jni$_.$JIntArray$NullableType$()); + } + + static final _id_getPackageGids$2 = _class.instanceMethodId( + r'getPackageGids', + r'(Ljava/lang/String;I)[I', + ); + + static final _getPackageGids$2 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract int[] getPackageGids(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JIntArray? getPackageGids$2(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getPackageGids$2( + reference.pointer, + _id_getPackageGids$2 as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const jni$_.$JIntArray$NullableType$()); + } + + static final _id_getPackageInfo = _class.instanceMethodId( + r'getPackageInfo', + r'(Landroid/content/pm/VersionedPackage;Landroid/content/pm/PackageManager$PackageInfoFlags;)Landroid/content/pm/PackageInfo;', + ); + + static final _getPackageInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.PackageInfo getPackageInfo(android.content.pm.VersionedPackage versionedPackage, android.content.pm.PackageManager$PackageInfoFlags packageInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPackageInfo( + jni$_.JObject? versionedPackage, + PackageManager$PackageInfoFlags? packageInfoFlags, + ) { + final _$versionedPackage = + versionedPackage?.reference ?? jni$_.jNullReference; + final _$packageInfoFlags = + packageInfoFlags?.reference ?? jni$_.jNullReference; + return _getPackageInfo( + reference.pointer, + _id_getPackageInfo as jni$_.JMethodIDPtr, + _$versionedPackage.pointer, + _$packageInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPackageInfo$1 = _class.instanceMethodId( + r'getPackageInfo', + r'(Landroid/content/pm/VersionedPackage;I)Landroid/content/pm/PackageInfo;', + ); + + static final _getPackageInfo$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.PackageInfo getPackageInfo(android.content.pm.VersionedPackage versionedPackage, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPackageInfo$1(jni$_.JObject? versionedPackage, int i) { + final _$versionedPackage = + versionedPackage?.reference ?? jni$_.jNullReference; + return _getPackageInfo$1( + reference.pointer, + _id_getPackageInfo$1 as jni$_.JMethodIDPtr, + _$versionedPackage.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPackageInfo$2 = _class.instanceMethodId( + r'getPackageInfo', + r'(Ljava/lang/String;Landroid/content/pm/PackageManager$PackageInfoFlags;)Landroid/content/pm/PackageInfo;', + ); + + static final _getPackageInfo$2 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.PackageInfo getPackageInfo(java.lang.String string, android.content.pm.PackageManager$PackageInfoFlags packageInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPackageInfo$2( + jni$_.JString? string, + PackageManager$PackageInfoFlags? packageInfoFlags, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$packageInfoFlags = + packageInfoFlags?.reference ?? jni$_.jNullReference; + return _getPackageInfo$2( + reference.pointer, + _id_getPackageInfo$2 as jni$_.JMethodIDPtr, + _$string.pointer, + _$packageInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPackageInfo$3 = _class.instanceMethodId( + r'getPackageInfo', + r'(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;', + ); + + static final _getPackageInfo$3 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.PackageInfo getPackageInfo(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPackageInfo$3(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getPackageInfo$3( + reference.pointer, + _id_getPackageInfo$3 as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPackageInstaller = _class.instanceMethodId( + r'getPackageInstaller', + r'()Landroid/content/pm/PackageInstaller;', + ); + + static final _getPackageInstaller = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract android.content.pm.PackageInstaller getPackageInstaller()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPackageInstaller() { + return _getPackageInstaller( + reference.pointer, + _id_getPackageInstaller as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPackageUid = _class.instanceMethodId( + r'getPackageUid', + r'(Ljava/lang/String;Landroid/content/pm/PackageManager$PackageInfoFlags;)I', + ); + + static final _getPackageUid = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public int getPackageUid(java.lang.String string, android.content.pm.PackageManager$PackageInfoFlags packageInfoFlags)` + int getPackageUid( + jni$_.JString? string, + PackageManager$PackageInfoFlags? packageInfoFlags, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$packageInfoFlags = + packageInfoFlags?.reference ?? jni$_.jNullReference; + return _getPackageUid( + reference.pointer, + _id_getPackageUid as jni$_.JMethodIDPtr, + _$string.pointer, + _$packageInfoFlags.pointer, + ).integer; + } + + static final _id_getPackageUid$1 = _class.instanceMethodId( + r'getPackageUid', + r'(Ljava/lang/String;I)I', + ); + + static final _getPackageUid$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract int getPackageUid(java.lang.String string, int i)` + int getPackageUid$1(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getPackageUid$1( + reference.pointer, + _id_getPackageUid$1 as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).integer; + } + + static final _id_getPackagesForUid = _class.instanceMethodId( + r'getPackagesForUid', + r'(I)[Ljava/lang/String;', + ); + + static final _getPackagesForUid = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public abstract java.lang.String[] getPackagesForUid(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getPackagesForUid(int i) { + return _getPackagesForUid( + reference.pointer, + _id_getPackagesForUid as jni$_.JMethodIDPtr, + i, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JString$NullableType$(), + ), + ); + } + + static final _id_getPackagesHoldingPermissions = _class.instanceMethodId( + r'getPackagesHoldingPermissions', + r'([Ljava/lang/String;Landroid/content/pm/PackageManager$PackageInfoFlags;)Ljava/util/List;', + ); + + static final _getPackagesHoldingPermissions = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List getPackagesHoldingPermissions(java.lang.String[] strings, android.content.pm.PackageManager$PackageInfoFlags packageInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getPackagesHoldingPermissions( + jni$_.JArray? strings, + PackageManager$PackageInfoFlags? packageInfoFlags, + ) { + final _$strings = strings?.reference ?? jni$_.jNullReference; + final _$packageInfoFlags = + packageInfoFlags?.reference ?? jni$_.jNullReference; + return _getPackagesHoldingPermissions( + reference.pointer, + _id_getPackagesHoldingPermissions as jni$_.JMethodIDPtr, + _$strings.pointer, + _$packageInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getPackagesHoldingPermissions$1 = _class.instanceMethodId( + r'getPackagesHoldingPermissions', + r'([Ljava/lang/String;I)Ljava/util/List;', + ); + + static final _getPackagesHoldingPermissions$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract java.util.List getPackagesHoldingPermissions(java.lang.String[] strings, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getPackagesHoldingPermissions$1( + jni$_.JArray? strings, + int i, + ) { + final _$strings = strings?.reference ?? jni$_.jNullReference; + return _getPackagesHoldingPermissions$1( + reference.pointer, + _id_getPackagesHoldingPermissions$1 as jni$_.JMethodIDPtr, + _$strings.pointer, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getPermissionGroupInfo = _class.instanceMethodId( + r'getPermissionGroupInfo', + r'(Ljava/lang/String;I)Landroid/content/pm/PermissionGroupInfo;', + ); + + static final _getPermissionGroupInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.PermissionGroupInfo getPermissionGroupInfo(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPermissionGroupInfo(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getPermissionGroupInfo( + reference.pointer, + _id_getPermissionGroupInfo as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPermissionInfo = _class.instanceMethodId( + r'getPermissionInfo', + r'(Ljava/lang/String;I)Landroid/content/pm/PermissionInfo;', + ); + + static final _getPermissionInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.PermissionInfo getPermissionInfo(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getPermissionInfo(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getPermissionInfo( + reference.pointer, + _id_getPermissionInfo as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getPlatformPermissionsForGroup = _class.instanceMethodId( + r'getPlatformPermissionsForGroup', + r'(Ljava/lang/String;Ljava/util/concurrent/Executor;Ljava/util/function/Consumer;)V', + ); + + static final _getPlatformPermissionsForGroup = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void getPlatformPermissionsForGroup(java.lang.String string, java.util.concurrent.Executor executor, java.util.function.Consumer> consumer)` + void getPlatformPermissionsForGroup( + jni$_.JString? string, + jni$_.JObject? executor, + jni$_.JObject? consumer, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$executor = executor?.reference ?? jni$_.jNullReference; + final _$consumer = consumer?.reference ?? jni$_.jNullReference; + _getPlatformPermissionsForGroup( + reference.pointer, + _id_getPlatformPermissionsForGroup as jni$_.JMethodIDPtr, + _$string.pointer, + _$executor.pointer, + _$consumer.pointer, + ).check(); + } + + static final _id_getPreferredActivities = _class.instanceMethodId( + r'getPreferredActivities', + r'(Ljava/util/List;Ljava/util/List;Ljava/lang/String;)I', + ); + + static final _getPreferredActivities = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract int getPreferredActivities(java.util.List list, java.util.List list1, java.lang.String string)` + int getPreferredActivities( + jni$_.JList? list, + jni$_.JList? list1, + jni$_.JString? string, + ) { + final _$list = list?.reference ?? jni$_.jNullReference; + final _$list1 = list1?.reference ?? jni$_.jNullReference; + final _$string = string?.reference ?? jni$_.jNullReference; + return _getPreferredActivities( + reference.pointer, + _id_getPreferredActivities as jni$_.JMethodIDPtr, + _$list.pointer, + _$list1.pointer, + _$string.pointer, + ).integer; + } + + static final _id_getPreferredPackages = _class.instanceMethodId( + r'getPreferredPackages', + r'(I)Ljava/util/List;', + ); + + static final _getPreferredPackages = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public abstract java.util.List getPreferredPackages(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getPreferredPackages(int i) { + return _getPreferredPackages( + reference.pointer, + _id_getPreferredPackages as jni$_.JMethodIDPtr, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getProperty = _class.instanceMethodId( + r'getProperty', + r'(Ljava/lang/String;Landroid/content/ComponentName;)Landroid/content/pm/PackageManager$Property;', + ); + + static final _getProperty = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.PackageManager$Property getProperty(java.lang.String string, android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + PackageManager$Property? getProperty( + jni$_.JString? string, + jni$_.JObject? componentName, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getProperty( + reference.pointer, + _id_getProperty as jni$_.JMethodIDPtr, + _$string.pointer, + _$componentName.pointer, + ).object( + const $PackageManager$Property$NullableType$(), + ); + } + + static final _id_getProperty$1 = _class.instanceMethodId( + r'getProperty', + r'(Ljava/lang/String;Ljava/lang/String;)Landroid/content/pm/PackageManager$Property;', + ); + + static final _getProperty$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.PackageManager$Property getProperty(java.lang.String string, java.lang.String string1)` + /// The returned object must be released after use, by calling the [release] method. + PackageManager$Property? getProperty$1( + jni$_.JString? string, + jni$_.JString? string1, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _getProperty$1( + reference.pointer, + _id_getProperty$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + ).object( + const $PackageManager$Property$NullableType$(), + ); + } + + static final _id_getProviderInfo = _class.instanceMethodId( + r'getProviderInfo', + r'(Landroid/content/ComponentName;Landroid/content/pm/PackageManager$ComponentInfoFlags;)Landroid/content/pm/ProviderInfo;', + ); + + static final _getProviderInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.ProviderInfo getProviderInfo(android.content.ComponentName componentName, android.content.pm.PackageManager$ComponentInfoFlags componentInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getProviderInfo( + jni$_.JObject? componentName, + PackageManager$ComponentInfoFlags? componentInfoFlags, + ) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + final _$componentInfoFlags = + componentInfoFlags?.reference ?? jni$_.jNullReference; + return _getProviderInfo( + reference.pointer, + _id_getProviderInfo as jni$_.JMethodIDPtr, + _$componentName.pointer, + _$componentInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getProviderInfo$1 = _class.instanceMethodId( + r'getProviderInfo', + r'(Landroid/content/ComponentName;I)Landroid/content/pm/ProviderInfo;', + ); + + static final _getProviderInfo$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.ProviderInfo getProviderInfo(android.content.ComponentName componentName, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getProviderInfo$1(jni$_.JObject? componentName, int i) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getProviderInfo$1( + reference.pointer, + _id_getProviderInfo$1 as jni$_.JMethodIDPtr, + _$componentName.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getReceiverInfo = _class.instanceMethodId( + r'getReceiverInfo', + r'(Landroid/content/ComponentName;Landroid/content/pm/PackageManager$ComponentInfoFlags;)Landroid/content/pm/ActivityInfo;', + ); + + static final _getReceiverInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.ActivityInfo getReceiverInfo(android.content.ComponentName componentName, android.content.pm.PackageManager$ComponentInfoFlags componentInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getReceiverInfo( + jni$_.JObject? componentName, + PackageManager$ComponentInfoFlags? componentInfoFlags, + ) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + final _$componentInfoFlags = + componentInfoFlags?.reference ?? jni$_.jNullReference; + return _getReceiverInfo( + reference.pointer, + _id_getReceiverInfo as jni$_.JMethodIDPtr, + _$componentName.pointer, + _$componentInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getReceiverInfo$1 = _class.instanceMethodId( + r'getReceiverInfo', + r'(Landroid/content/ComponentName;I)Landroid/content/pm/ActivityInfo;', + ); + + static final _getReceiverInfo$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.ActivityInfo getReceiverInfo(android.content.ComponentName componentName, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getReceiverInfo$1(jni$_.JObject? componentName, int i) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getReceiverInfo$1( + reference.pointer, + _id_getReceiverInfo$1 as jni$_.JMethodIDPtr, + _$componentName.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getResourcesForActivity = _class.instanceMethodId( + r'getResourcesForActivity', + r'(Landroid/content/ComponentName;)Landroid/content/res/Resources;', + ); + + static final _getResourcesForActivity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.content.res.Resources getResourcesForActivity(android.content.ComponentName componentName)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getResourcesForActivity(jni$_.JObject? componentName) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getResourcesForActivity( + reference.pointer, + _id_getResourcesForActivity as jni$_.JMethodIDPtr, + _$componentName.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getResourcesForApplication = _class.instanceMethodId( + r'getResourcesForApplication', + r'(Landroid/content/pm/ApplicationInfo;)Landroid/content/res/Resources;', + ); + + static final _getResourcesForApplication = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.content.res.Resources getResourcesForApplication(android.content.pm.ApplicationInfo applicationInfo)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getResourcesForApplication(jni$_.JObject? applicationInfo) { + final _$applicationInfo = + applicationInfo?.reference ?? jni$_.jNullReference; + return _getResourcesForApplication( + reference.pointer, + _id_getResourcesForApplication as jni$_.JMethodIDPtr, + _$applicationInfo.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getResourcesForApplication$1 = _class.instanceMethodId( + r'getResourcesForApplication', + r'(Landroid/content/pm/ApplicationInfo;Landroid/content/res/Configuration;)Landroid/content/res/Resources;', + ); + + static final _getResourcesForApplication$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.res.Resources getResourcesForApplication(android.content.pm.ApplicationInfo applicationInfo, android.content.res.Configuration configuration)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getResourcesForApplication$1( + jni$_.JObject? applicationInfo, + jni$_.JObject? configuration, + ) { + final _$applicationInfo = + applicationInfo?.reference ?? jni$_.jNullReference; + final _$configuration = configuration?.reference ?? jni$_.jNullReference; + return _getResourcesForApplication$1( + reference.pointer, + _id_getResourcesForApplication$1 as jni$_.JMethodIDPtr, + _$applicationInfo.pointer, + _$configuration.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getResourcesForApplication$2 = _class.instanceMethodId( + r'getResourcesForApplication', + r'(Ljava/lang/String;)Landroid/content/res/Resources;', + ); + + static final _getResourcesForApplication$2 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.content.res.Resources getResourcesForApplication(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getResourcesForApplication$2(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getResourcesForApplication$2( + reference.pointer, + _id_getResourcesForApplication$2 as jni$_.JMethodIDPtr, + _$string.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getServiceInfo = _class.instanceMethodId( + r'getServiceInfo', + r'(Landroid/content/ComponentName;Landroid/content/pm/PackageManager$ComponentInfoFlags;)Landroid/content/pm/ServiceInfo;', + ); + + static final _getServiceInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.ServiceInfo getServiceInfo(android.content.ComponentName componentName, android.content.pm.PackageManager$ComponentInfoFlags componentInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getServiceInfo( + jni$_.JObject? componentName, + PackageManager$ComponentInfoFlags? componentInfoFlags, + ) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + final _$componentInfoFlags = + componentInfoFlags?.reference ?? jni$_.jNullReference; + return _getServiceInfo( + reference.pointer, + _id_getServiceInfo as jni$_.JMethodIDPtr, + _$componentName.pointer, + _$componentInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getServiceInfo$1 = _class.instanceMethodId( + r'getServiceInfo', + r'(Landroid/content/ComponentName;I)Landroid/content/pm/ServiceInfo;', + ); + + static final _getServiceInfo$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.ServiceInfo getServiceInfo(android.content.ComponentName componentName, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getServiceInfo$1(jni$_.JObject? componentName, int i) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + return _getServiceInfo$1( + reference.pointer, + _id_getServiceInfo$1 as jni$_.JMethodIDPtr, + _$componentName.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSharedLibraries = _class.instanceMethodId( + r'getSharedLibraries', + r'(Landroid/content/pm/PackageManager$PackageInfoFlags;)Ljava/util/List;', + ); + + static final _getSharedLibraries = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List getSharedLibraries(android.content.pm.PackageManager$PackageInfoFlags packageInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getSharedLibraries( + PackageManager$PackageInfoFlags? packageInfoFlags, + ) { + final _$packageInfoFlags = + packageInfoFlags?.reference ?? jni$_.jNullReference; + return _getSharedLibraries( + reference.pointer, + _id_getSharedLibraries as jni$_.JMethodIDPtr, + _$packageInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getSharedLibraries$1 = _class.instanceMethodId( + r'getSharedLibraries', + r'(I)Ljava/util/List;', + ); + + static final _getSharedLibraries$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + ) + >(); + + /// from: `public abstract java.util.List getSharedLibraries(int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? getSharedLibraries$1(int i) { + return _getSharedLibraries$1( + reference.pointer, + _id_getSharedLibraries$1 as jni$_.JMethodIDPtr, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getSuspendedPackageAppExtras = _class.instanceMethodId( + r'getSuspendedPackageAppExtras', + r'()Landroid/os/Bundle;', + ); + + static final _getSuspendedPackageAppExtras = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public android.os.Bundle getSuspendedPackageAppExtras()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getSuspendedPackageAppExtras() { + return _getSuspendedPackageAppExtras( + reference.pointer, + _id_getSuspendedPackageAppExtras as jni$_.JMethodIDPtr, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getSyntheticAppDetailsActivityEnabled = _class + .instanceMethodId( + r'getSyntheticAppDetailsActivityEnabled', + r'(Ljava/lang/String;)Z', + ); + + static final _getSyntheticAppDetailsActivityEnabled = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean getSyntheticAppDetailsActivityEnabled(java.lang.String string)` + bool getSyntheticAppDetailsActivityEnabled(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getSyntheticAppDetailsActivityEnabled( + reference.pointer, + _id_getSyntheticAppDetailsActivityEnabled as jni$_.JMethodIDPtr, + _$string.pointer, + ).boolean; + } + + static final _id_getSystemAvailableFeatures = _class.instanceMethodId( + r'getSystemAvailableFeatures', + r'()[Landroid/content/pm/FeatureInfo;', + ); + + static final _getSystemAvailableFeatures = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract android.content.pm.FeatureInfo[] getSystemAvailableFeatures()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getSystemAvailableFeatures() { + return _getSystemAvailableFeatures( + reference.pointer, + _id_getSystemAvailableFeatures as jni$_.JMethodIDPtr, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_getSystemSharedLibraryNames = _class.instanceMethodId( + r'getSystemSharedLibraryNames', + r'()[Ljava/lang/String;', + ); + + static final _getSystemSharedLibraryNames = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract java.lang.String[] getSystemSharedLibraryNames()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JArray? getSystemSharedLibraryNames() { + return _getSystemSharedLibraryNames( + reference.pointer, + _id_getSystemSharedLibraryNames as jni$_.JMethodIDPtr, + ).object?>( + const jni$_.$JArray$NullableType$( + jni$_.$JString$NullableType$(), + ), + ); + } + + static final _id_getTargetSdkVersion = _class.instanceMethodId( + r'getTargetSdkVersion', + r'(Ljava/lang/String;)I', + ); + + static final _getTargetSdkVersion = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public int getTargetSdkVersion(java.lang.String string)` + int getTargetSdkVersion(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getTargetSdkVersion( + reference.pointer, + _id_getTargetSdkVersion as jni$_.JMethodIDPtr, + _$string.pointer, + ).integer; + } + + static final _id_getText = _class.instanceMethodId( + r'getText', + r'(Ljava/lang/String;ILandroid/content/pm/ApplicationInfo;)Ljava/lang/CharSequence;', + ); + + static final _getText = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract java.lang.CharSequence getText(java.lang.String string, int i, android.content.pm.ApplicationInfo applicationInfo)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getText( + jni$_.JString? string, + int i, + jni$_.JObject? applicationInfo, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$applicationInfo = + applicationInfo?.reference ?? jni$_.jNullReference; + return _getText( + reference.pointer, + _id_getText as jni$_.JMethodIDPtr, + _$string.pointer, + i, + _$applicationInfo.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getUserBadgedDrawableForDensity = _class.instanceMethodId( + r'getUserBadgedDrawableForDensity', + r'(Landroid/graphics/drawable/Drawable;Landroid/os/UserHandle;Landroid/graphics/Rect;I)Landroid/graphics/drawable/Drawable;', + ); + + static final _getUserBadgedDrawableForDensity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getUserBadgedDrawableForDensity(android.graphics.drawable.Drawable drawable, android.os.UserHandle userHandle, android.graphics.Rect rect, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getUserBadgedDrawableForDensity( + jni$_.JObject? drawable, + jni$_.JObject? userHandle, + jni$_.JObject? rect, + int i, + ) { + final _$drawable = drawable?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + final _$rect = rect?.reference ?? jni$_.jNullReference; + return _getUserBadgedDrawableForDensity( + reference.pointer, + _id_getUserBadgedDrawableForDensity as jni$_.JMethodIDPtr, + _$drawable.pointer, + _$userHandle.pointer, + _$rect.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getUserBadgedIcon = _class.instanceMethodId( + r'getUserBadgedIcon', + r'(Landroid/graphics/drawable/Drawable;Landroid/os/UserHandle;)Landroid/graphics/drawable/Drawable;', + ); + + static final _getUserBadgedIcon = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.graphics.drawable.Drawable getUserBadgedIcon(android.graphics.drawable.Drawable drawable, android.os.UserHandle userHandle)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getUserBadgedIcon( + jni$_.JObject? drawable, + jni$_.JObject? userHandle, + ) { + final _$drawable = drawable?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + return _getUserBadgedIcon( + reference.pointer, + _id_getUserBadgedIcon as jni$_.JMethodIDPtr, + _$drawable.pointer, + _$userHandle.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getUserBadgedLabel = _class.instanceMethodId( + r'getUserBadgedLabel', + r'(Ljava/lang/CharSequence;Landroid/os/UserHandle;)Ljava/lang/CharSequence;', + ); + + static final _getUserBadgedLabel = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract java.lang.CharSequence getUserBadgedLabel(java.lang.CharSequence charSequence, android.os.UserHandle userHandle)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getUserBadgedLabel( + jni$_.JObject? charSequence, + jni$_.JObject? userHandle, + ) { + final _$charSequence = charSequence?.reference ?? jni$_.jNullReference; + final _$userHandle = userHandle?.reference ?? jni$_.jNullReference; + return _getUserBadgedLabel( + reference.pointer, + _id_getUserBadgedLabel as jni$_.JMethodIDPtr, + _$charSequence.pointer, + _$userHandle.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getVerifiedSigningInfo = _class.staticMethodId( + r'getVerifiedSigningInfo', + r'(Ljava/lang/String;I)Landroid/content/pm/SigningInfo;', + ); + + static final _getVerifiedSigningInfo = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `static public android.content.pm.SigningInfo getVerifiedSigningInfo(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getVerifiedSigningInfo(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getVerifiedSigningInfo( + _class.reference.pointer, + _id_getVerifiedSigningInfo as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_getWhitelistedRestrictedPermissions = _class + .instanceMethodId( + r'getWhitelistedRestrictedPermissions', + r'(Ljava/lang/String;I)Ljava/util/Set;', + ); + + static final _getWhitelistedRestrictedPermissions = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public java.util.Set getWhitelistedRestrictedPermissions(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JSet? getWhitelistedRestrictedPermissions( + jni$_.JString? string, + int i, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _getWhitelistedRestrictedPermissions( + reference.pointer, + _id_getWhitelistedRestrictedPermissions as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object?>( + const jni$_.$JSet$NullableType$( + jni$_.$JString$NullableType$(), + ), + ); + } + + static final _id_getXml = _class.instanceMethodId( + r'getXml', + r'(Ljava/lang/String;ILandroid/content/pm/ApplicationInfo;)Landroid/content/res/XmlResourceParser;', + ); + + static final _getXml = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract android.content.res.XmlResourceParser getXml(java.lang.String string, int i, android.content.pm.ApplicationInfo applicationInfo)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? getXml( + jni$_.JString? string, + int i, + jni$_.JObject? applicationInfo, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$applicationInfo = + applicationInfo?.reference ?? jni$_.jNullReference; + return _getXml( + reference.pointer, + _id_getXml as jni$_.JMethodIDPtr, + _$string.pointer, + i, + _$applicationInfo.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_hasSigningCertificate = _class.instanceMethodId( + r'hasSigningCertificate', + r'(I[BI)Z', + ); + + static final _hasSigningCertificate = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Int32, jni$_.Pointer, jni$_.Int32) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public boolean hasSigningCertificate(int i, byte[] bs, int i1)` + bool hasSigningCertificate(int i, jni$_.JByteArray? bs, int i1) { + final _$bs = bs?.reference ?? jni$_.jNullReference; + return _hasSigningCertificate( + reference.pointer, + _id_hasSigningCertificate as jni$_.JMethodIDPtr, + i, + _$bs.pointer, + i1, + ).boolean; + } + + static final _id_hasSigningCertificate$1 = _class.instanceMethodId( + r'hasSigningCertificate', + r'(Ljava/lang/String;[BI)Z', + ); + + static final _hasSigningCertificate$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public boolean hasSigningCertificate(java.lang.String string, byte[] bs, int i)` + bool hasSigningCertificate$1( + jni$_.JString? string, + jni$_.JByteArray? bs, + int i, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$bs = bs?.reference ?? jni$_.jNullReference; + return _hasSigningCertificate$1( + reference.pointer, + _id_hasSigningCertificate$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$bs.pointer, + i, + ).boolean; + } + + static final _id_hasSystemFeature = _class.instanceMethodId( + r'hasSystemFeature', + r'(Ljava/lang/String;)Z', + ); + + static final _hasSystemFeature = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract boolean hasSystemFeature(java.lang.String string)` + bool hasSystemFeature(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _hasSystemFeature( + reference.pointer, + _id_hasSystemFeature as jni$_.JMethodIDPtr, + _$string.pointer, + ).boolean; + } + + static final _id_hasSystemFeature$1 = _class.instanceMethodId( + r'hasSystemFeature', + r'(Ljava/lang/String;I)Z', + ); + + static final _hasSystemFeature$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract boolean hasSystemFeature(java.lang.String string, int i)` + bool hasSystemFeature$1(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _hasSystemFeature$1( + reference.pointer, + _id_hasSystemFeature$1 as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).boolean; + } + + static final _id_isAppArchivable = _class.instanceMethodId( + r'isAppArchivable', + r'(Ljava/lang/String;)Z', + ); + + static final _isAppArchivable = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean isAppArchivable(java.lang.String string)` + bool isAppArchivable(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _isAppArchivable( + reference.pointer, + _id_isAppArchivable as jni$_.JMethodIDPtr, + _$string.pointer, + ).boolean; + } + + static final _id_isAutoRevokeWhitelisted = _class.instanceMethodId( + r'isAutoRevokeWhitelisted', + r'()Z', + ); + + static final _isAutoRevokeWhitelisted = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean isAutoRevokeWhitelisted()` + bool isAutoRevokeWhitelisted() { + return _isAutoRevokeWhitelisted( + reference.pointer, + _id_isAutoRevokeWhitelisted as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_isAutoRevokeWhitelisted$1 = _class.instanceMethodId( + r'isAutoRevokeWhitelisted', + r'(Ljava/lang/String;)Z', + ); + + static final _isAutoRevokeWhitelisted$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean isAutoRevokeWhitelisted(java.lang.String string)` + bool isAutoRevokeWhitelisted$1(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _isAutoRevokeWhitelisted$1( + reference.pointer, + _id_isAutoRevokeWhitelisted$1 as jni$_.JMethodIDPtr, + _$string.pointer, + ).boolean; + } + + static final _id_isDefaultApplicationIcon = _class.instanceMethodId( + r'isDefaultApplicationIcon', + r'(Landroid/graphics/drawable/Drawable;)Z', + ); + + static final _isDefaultApplicationIcon = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean isDefaultApplicationIcon(android.graphics.drawable.Drawable drawable)` + bool isDefaultApplicationIcon(jni$_.JObject? drawable) { + final _$drawable = drawable?.reference ?? jni$_.jNullReference; + return _isDefaultApplicationIcon( + reference.pointer, + _id_isDefaultApplicationIcon as jni$_.JMethodIDPtr, + _$drawable.pointer, + ).boolean; + } + + static final _id_isDeviceUpgrading = _class.instanceMethodId( + r'isDeviceUpgrading', + r'()Z', + ); + + static final _isDeviceUpgrading = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean isDeviceUpgrading()` + bool isDeviceUpgrading() { + return _isDeviceUpgrading( + reference.pointer, + _id_isDeviceUpgrading as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_isInstantApp = _class.instanceMethodId( + r'isInstantApp', + r'()Z', + ); + + static final _isInstantApp = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract boolean isInstantApp()` + bool isInstantApp() { + return _isInstantApp( + reference.pointer, + _id_isInstantApp as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_isInstantApp$1 = _class.instanceMethodId( + r'isInstantApp', + r'(Ljava/lang/String;)Z', + ); + + static final _isInstantApp$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract boolean isInstantApp(java.lang.String string)` + bool isInstantApp$1(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _isInstantApp$1( + reference.pointer, + _id_isInstantApp$1 as jni$_.JMethodIDPtr, + _$string.pointer, + ).boolean; + } + + static final _id_isPackageStopped = _class.instanceMethodId( + r'isPackageStopped', + r'(Ljava/lang/String;)Z', + ); + + static final _isPackageStopped = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean isPackageStopped(java.lang.String string)` + bool isPackageStopped(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _isPackageStopped( + reference.pointer, + _id_isPackageStopped as jni$_.JMethodIDPtr, + _$string.pointer, + ).boolean; + } + + static final _id_isPackageSuspended = _class.instanceMethodId( + r'isPackageSuspended', + r'()Z', + ); + + static final _isPackageSuspended = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean isPackageSuspended()` + bool isPackageSuspended() { + return _isPackageSuspended( + reference.pointer, + _id_isPackageSuspended as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_isPackageSuspended$1 = _class.instanceMethodId( + r'isPackageSuspended', + r'(Ljava/lang/String;)Z', + ); + + static final _isPackageSuspended$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean isPackageSuspended(java.lang.String string)` + bool isPackageSuspended$1(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _isPackageSuspended$1( + reference.pointer, + _id_isPackageSuspended$1 as jni$_.JMethodIDPtr, + _$string.pointer, + ).boolean; + } + + static final _id_isPermissionRevokedByPolicy = _class.instanceMethodId( + r'isPermissionRevokedByPolicy', + r'(Ljava/lang/String;Ljava/lang/String;)Z', + ); + + static final _isPermissionRevokedByPolicy = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract boolean isPermissionRevokedByPolicy(java.lang.String string, java.lang.String string1)` + bool isPermissionRevokedByPolicy( + jni$_.JString? string, + jni$_.JString? string1, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _isPermissionRevokedByPolicy( + reference.pointer, + _id_isPermissionRevokedByPolicy as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + ).boolean; + } + + static final _id_isSafeMode = _class.instanceMethodId(r'isSafeMode', r'()Z'); + + static final _isSafeMode = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract boolean isSafeMode()` + bool isSafeMode() { + return _isSafeMode( + reference.pointer, + _id_isSafeMode as jni$_.JMethodIDPtr, + ).boolean; + } + + static final _id_parseAndroidManifest = _class.instanceMethodId( + r'parseAndroidManifest', + r'(Landroid/os/ParcelFileDescriptor;Ljava/util/function/Function;)Ljava/lang/Object;', + ); + + static final _parseAndroidManifest = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public T parseAndroidManifest(android.os.ParcelFileDescriptor parcelFileDescriptor, java.util.function.Function function)` + /// The returned object must be released after use, by calling the [release] method. + $T? parseAndroidManifest<$T extends jni$_.JObject?>( + jni$_.JObject? parcelFileDescriptor, + jni$_.JObject? function, { + required jni$_.JType<$T> T, + }) { + final _$parcelFileDescriptor = + parcelFileDescriptor?.reference ?? jni$_.jNullReference; + final _$function = function?.reference ?? jni$_.jNullReference; + return _parseAndroidManifest( + reference.pointer, + _id_parseAndroidManifest as jni$_.JMethodIDPtr, + _$parcelFileDescriptor.pointer, + _$function.pointer, + ).object<$T?>(T.nullableType); + } + + static final _id_parseAndroidManifest$1 = _class.instanceMethodId( + r'parseAndroidManifest', + r'(Ljava/io/File;Ljava/util/function/Function;)Ljava/lang/Object;', + ); + + static final _parseAndroidManifest$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public T parseAndroidManifest(java.io.File file, java.util.function.Function function)` + /// The returned object must be released after use, by calling the [release] method. + $T? parseAndroidManifest$1<$T extends jni$_.JObject?>( + jni$_.JObject? file, + jni$_.JObject? function, { + required jni$_.JType<$T> T, + }) { + final _$file = file?.reference ?? jni$_.jNullReference; + final _$function = function?.reference ?? jni$_.jNullReference; + return _parseAndroidManifest$1( + reference.pointer, + _id_parseAndroidManifest$1 as jni$_.JMethodIDPtr, + _$file.pointer, + _$function.pointer, + ).object<$T?>(T.nullableType); + } + + static final _id_queryActivityProperty = _class.instanceMethodId( + r'queryActivityProperty', + r'(Ljava/lang/String;)Ljava/util/List;', + ); + + static final _queryActivityProperty = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryActivityProperty(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryActivityProperty( + jni$_.JString? string, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _queryActivityProperty( + reference.pointer, + _id_queryActivityProperty as jni$_.JMethodIDPtr, + _$string.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + $PackageManager$Property$NullableType$(), + ), + ); + } + + static final _id_queryApplicationProperty = _class.instanceMethodId( + r'queryApplicationProperty', + r'(Ljava/lang/String;)Ljava/util/List;', + ); + + static final _queryApplicationProperty = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryApplicationProperty(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryApplicationProperty( + jni$_.JString? string, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _queryApplicationProperty( + reference.pointer, + _id_queryApplicationProperty as jni$_.JMethodIDPtr, + _$string.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + $PackageManager$Property$NullableType$(), + ), + ); + } + + static final _id_queryBroadcastReceivers = _class.instanceMethodId( + r'queryBroadcastReceivers', + r'(Landroid/content/Intent;Landroid/content/pm/PackageManager$ResolveInfoFlags;)Ljava/util/List;', + ); + + static final _queryBroadcastReceivers = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryBroadcastReceivers(android.content.Intent intent, android.content.pm.PackageManager$ResolveInfoFlags resolveInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryBroadcastReceivers( + Intent? intent, + PackageManager$ResolveInfoFlags? resolveInfoFlags, + ) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$resolveInfoFlags = + resolveInfoFlags?.reference ?? jni$_.jNullReference; + return _queryBroadcastReceivers( + reference.pointer, + _id_queryBroadcastReceivers as jni$_.JMethodIDPtr, + _$intent.pointer, + _$resolveInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryBroadcastReceivers$1 = _class.instanceMethodId( + r'queryBroadcastReceivers', + r'(Landroid/content/Intent;I)Ljava/util/List;', + ); + + static final _queryBroadcastReceivers$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract java.util.List queryBroadcastReceivers(android.content.Intent intent, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryBroadcastReceivers$1( + Intent? intent, + int i, + ) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _queryBroadcastReceivers$1( + reference.pointer, + _id_queryBroadcastReceivers$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryContentProviders = _class.instanceMethodId( + r'queryContentProviders', + r'(Ljava/lang/String;ILandroid/content/pm/PackageManager$ComponentInfoFlags;)Ljava/util/List;', + ); + + static final _queryContentProviders = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryContentProviders(java.lang.String string, int i, android.content.pm.PackageManager$ComponentInfoFlags componentInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryContentProviders( + jni$_.JString? string, + int i, + PackageManager$ComponentInfoFlags? componentInfoFlags, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$componentInfoFlags = + componentInfoFlags?.reference ?? jni$_.jNullReference; + return _queryContentProviders( + reference.pointer, + _id_queryContentProviders as jni$_.JMethodIDPtr, + _$string.pointer, + i, + _$componentInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryContentProviders$1 = _class.instanceMethodId( + r'queryContentProviders', + r'(Ljava/lang/String;II)Ljava/util/List;', + ); + + static final _queryContentProviders$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Int32, jni$_.Int32) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + int, + ) + >(); + + /// from: `public abstract java.util.List queryContentProviders(java.lang.String string, int i, int i1)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryContentProviders$1( + jni$_.JString? string, + int i, + int i1, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _queryContentProviders$1( + reference.pointer, + _id_queryContentProviders$1 as jni$_.JMethodIDPtr, + _$string.pointer, + i, + i1, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryInstrumentation = _class.instanceMethodId( + r'queryInstrumentation', + r'(Ljava/lang/String;I)Ljava/util/List;', + ); + + static final _queryInstrumentation = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract java.util.List queryInstrumentation(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryInstrumentation( + jni$_.JString? string, + int i, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _queryInstrumentation( + reference.pointer, + _id_queryInstrumentation as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryIntentActivities = _class.instanceMethodId( + r'queryIntentActivities', + r'(Landroid/content/Intent;Landroid/content/pm/PackageManager$ResolveInfoFlags;)Ljava/util/List;', + ); + + static final _queryIntentActivities = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryIntentActivities(android.content.Intent intent, android.content.pm.PackageManager$ResolveInfoFlags resolveInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryIntentActivities( + Intent? intent, + PackageManager$ResolveInfoFlags? resolveInfoFlags, + ) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$resolveInfoFlags = + resolveInfoFlags?.reference ?? jni$_.jNullReference; + return _queryIntentActivities( + reference.pointer, + _id_queryIntentActivities as jni$_.JMethodIDPtr, + _$intent.pointer, + _$resolveInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryIntentActivities$1 = _class.instanceMethodId( + r'queryIntentActivities', + r'(Landroid/content/Intent;I)Ljava/util/List;', + ); + + static final _queryIntentActivities$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract java.util.List queryIntentActivities(android.content.Intent intent, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryIntentActivities$1(Intent? intent, int i) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _queryIntentActivities$1( + reference.pointer, + _id_queryIntentActivities$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryIntentActivityOptions = _class.instanceMethodId( + r'queryIntentActivityOptions', + r'(Landroid/content/ComponentName;[Landroid/content/Intent;Landroid/content/Intent;I)Ljava/util/List;', + ); + + static final _queryIntentActivityOptions = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract java.util.List queryIntentActivityOptions(android.content.ComponentName componentName, android.content.Intent[] intents, android.content.Intent intent, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryIntentActivityOptions( + jni$_.JObject? componentName, + jni$_.JArray? intents, + Intent? intent, + int i, + ) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + final _$intents = intents?.reference ?? jni$_.jNullReference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _queryIntentActivityOptions( + reference.pointer, + _id_queryIntentActivityOptions as jni$_.JMethodIDPtr, + _$componentName.pointer, + _$intents.pointer, + _$intent.pointer, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryIntentActivityOptions$1 = _class.instanceMethodId( + r'queryIntentActivityOptions', + r'(Landroid/content/ComponentName;Ljava/util/List;Landroid/content/Intent;Landroid/content/pm/PackageManager$ResolveInfoFlags;)Ljava/util/List;', + ); + + static final _queryIntentActivityOptions$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryIntentActivityOptions(android.content.ComponentName componentName, java.util.List list, android.content.Intent intent, android.content.pm.PackageManager$ResolveInfoFlags resolveInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryIntentActivityOptions$1( + jni$_.JObject? componentName, + jni$_.JList? list, + Intent? intent, + PackageManager$ResolveInfoFlags? resolveInfoFlags, + ) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + final _$list = list?.reference ?? jni$_.jNullReference; + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$resolveInfoFlags = + resolveInfoFlags?.reference ?? jni$_.jNullReference; + return _queryIntentActivityOptions$1( + reference.pointer, + _id_queryIntentActivityOptions$1 as jni$_.JMethodIDPtr, + _$componentName.pointer, + _$list.pointer, + _$intent.pointer, + _$resolveInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryIntentContentProviders = _class.instanceMethodId( + r'queryIntentContentProviders', + r'(Landroid/content/Intent;Landroid/content/pm/PackageManager$ResolveInfoFlags;)Ljava/util/List;', + ); + + static final _queryIntentContentProviders = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryIntentContentProviders(android.content.Intent intent, android.content.pm.PackageManager$ResolveInfoFlags resolveInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryIntentContentProviders( + Intent? intent, + PackageManager$ResolveInfoFlags? resolveInfoFlags, + ) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$resolveInfoFlags = + resolveInfoFlags?.reference ?? jni$_.jNullReference; + return _queryIntentContentProviders( + reference.pointer, + _id_queryIntentContentProviders as jni$_.JMethodIDPtr, + _$intent.pointer, + _$resolveInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryIntentContentProviders$1 = _class.instanceMethodId( + r'queryIntentContentProviders', + r'(Landroid/content/Intent;I)Ljava/util/List;', + ); + + static final _queryIntentContentProviders$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract java.util.List queryIntentContentProviders(android.content.Intent intent, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryIntentContentProviders$1( + Intent? intent, + int i, + ) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _queryIntentContentProviders$1( + reference.pointer, + _id_queryIntentContentProviders$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryIntentServices = _class.instanceMethodId( + r'queryIntentServices', + r'(Landroid/content/Intent;Landroid/content/pm/PackageManager$ResolveInfoFlags;)Ljava/util/List;', + ); + + static final _queryIntentServices = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryIntentServices(android.content.Intent intent, android.content.pm.PackageManager$ResolveInfoFlags resolveInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryIntentServices( + Intent? intent, + PackageManager$ResolveInfoFlags? resolveInfoFlags, + ) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$resolveInfoFlags = + resolveInfoFlags?.reference ?? jni$_.jNullReference; + return _queryIntentServices( + reference.pointer, + _id_queryIntentServices as jni$_.JMethodIDPtr, + _$intent.pointer, + _$resolveInfoFlags.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryIntentServices$1 = _class.instanceMethodId( + r'queryIntentServices', + r'(Landroid/content/Intent;I)Ljava/util/List;', + ); + + static final _queryIntentServices$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract java.util.List queryIntentServices(android.content.Intent intent, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryIntentServices$1(Intent? intent, int i) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _queryIntentServices$1( + reference.pointer, + _id_queryIntentServices$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryPermissionsByGroup = _class.instanceMethodId( + r'queryPermissionsByGroup', + r'(Ljava/lang/String;I)Ljava/util/List;', + ); + + static final _queryPermissionsByGroup = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract java.util.List queryPermissionsByGroup(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryPermissionsByGroup( + jni$_.JString? string, + int i, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _queryPermissionsByGroup( + reference.pointer, + _id_queryPermissionsByGroup as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object?>( + const jni$_.$JList$NullableType$( + jni$_.$JObject$NullableType$(), + ), + ); + } + + static final _id_queryProviderProperty = _class.instanceMethodId( + r'queryProviderProperty', + r'(Ljava/lang/String;)Ljava/util/List;', + ); + + static final _queryProviderProperty = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryProviderProperty(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryProviderProperty( + jni$_.JString? string, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _queryProviderProperty( + reference.pointer, + _id_queryProviderProperty as jni$_.JMethodIDPtr, + _$string.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + $PackageManager$Property$NullableType$(), + ), + ); + } + + static final _id_queryReceiverProperty = _class.instanceMethodId( + r'queryReceiverProperty', + r'(Ljava/lang/String;)Ljava/util/List;', + ); + + static final _queryReceiverProperty = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryReceiverProperty(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryReceiverProperty( + jni$_.JString? string, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _queryReceiverProperty( + reference.pointer, + _id_queryReceiverProperty as jni$_.JMethodIDPtr, + _$string.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + $PackageManager$Property$NullableType$(), + ), + ); + } + + static final _id_queryServiceProperty = _class.instanceMethodId( + r'queryServiceProperty', + r'(Ljava/lang/String;)Ljava/util/List;', + ); + + static final _queryServiceProperty = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public java.util.List queryServiceProperty(java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JList? queryServiceProperty( + jni$_.JString? string, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _queryServiceProperty( + reference.pointer, + _id_queryServiceProperty as jni$_.JMethodIDPtr, + _$string.pointer, + ).object?>( + const jni$_.$JList$NullableType$( + $PackageManager$Property$NullableType$(), + ), + ); + } + + static final _id_relinquishUpdateOwnership = _class.instanceMethodId( + r'relinquishUpdateOwnership', + r'(Ljava/lang/String;)V', + ); + + static final _relinquishUpdateOwnership = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void relinquishUpdateOwnership(java.lang.String string)` + void relinquishUpdateOwnership(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + _relinquishUpdateOwnership( + reference.pointer, + _id_relinquishUpdateOwnership as jni$_.JMethodIDPtr, + _$string.pointer, + ).check(); + } + + static final _id_removePackageFromPreferred = _class.instanceMethodId( + r'removePackageFromPreferred', + r'(Ljava/lang/String;)V', + ); + + static final _removePackageFromPreferred = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void removePackageFromPreferred(java.lang.String string)` + void removePackageFromPreferred(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + _removePackageFromPreferred( + reference.pointer, + _id_removePackageFromPreferred as jni$_.JMethodIDPtr, + _$string.pointer, + ).check(); + } + + static final _id_removePermission = _class.instanceMethodId( + r'removePermission', + r'(Ljava/lang/String;)V', + ); + + static final _removePermission = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void removePermission(java.lang.String string)` + void removePermission(jni$_.JString? string) { + final _$string = string?.reference ?? jni$_.jNullReference; + _removePermission( + reference.pointer, + _id_removePermission as jni$_.JMethodIDPtr, + _$string.pointer, + ).check(); + } + + static final _id_removeWhitelistedRestrictedPermission = _class + .instanceMethodId( + r'removeWhitelistedRestrictedPermission', + r'(Ljava/lang/String;Ljava/lang/String;I)Z', + ); + + static final _removeWhitelistedRestrictedPermission = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public boolean removeWhitelistedRestrictedPermission(java.lang.String string, java.lang.String string1, int i)` + bool removeWhitelistedRestrictedPermission( + jni$_.JString? string, + jni$_.JString? string1, + int i, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + return _removeWhitelistedRestrictedPermission( + reference.pointer, + _id_removeWhitelistedRestrictedPermission as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + i, + ).boolean; + } + + static final _id_requestChecksums = _class.instanceMethodId( + r'requestChecksums', + r'(Ljava/lang/String;ZILjava/util/List;Landroid/content/pm/PackageManager$OnChecksumsReadyListener;)V', + ); + + static final _requestChecksums = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Int32, + jni$_.Int32, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + int, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void requestChecksums(java.lang.String string, boolean z, int i, java.util.List list, android.content.pm.PackageManager$OnChecksumsReadyListener onChecksumsReadyListener)` + void requestChecksums( + jni$_.JString? string, + bool z, + int i, + jni$_.JList? list, + PackageManager$OnChecksumsReadyListener? onChecksumsReadyListener, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$list = list?.reference ?? jni$_.jNullReference; + final _$onChecksumsReadyListener = + onChecksumsReadyListener?.reference ?? jni$_.jNullReference; + _requestChecksums( + reference.pointer, + _id_requestChecksums as jni$_.JMethodIDPtr, + _$string.pointer, + z ? 1 : 0, + i, + _$list.pointer, + _$onChecksumsReadyListener.pointer, + ).check(); + } + + static final _id_resolveActivity = _class.instanceMethodId( + r'resolveActivity', + r'(Landroid/content/Intent;Landroid/content/pm/PackageManager$ResolveInfoFlags;)Landroid/content/pm/ResolveInfo;', + ); + + static final _resolveActivity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.ResolveInfo resolveActivity(android.content.Intent intent, android.content.pm.PackageManager$ResolveInfoFlags resolveInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveActivity( + Intent? intent, + PackageManager$ResolveInfoFlags? resolveInfoFlags, + ) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$resolveInfoFlags = + resolveInfoFlags?.reference ?? jni$_.jNullReference; + return _resolveActivity( + reference.pointer, + _id_resolveActivity as jni$_.JMethodIDPtr, + _$intent.pointer, + _$resolveInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_resolveActivity$1 = _class.instanceMethodId( + r'resolveActivity', + r'(Landroid/content/Intent;I)Landroid/content/pm/ResolveInfo;', + ); + + static final _resolveActivity$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.ResolveInfo resolveActivity(android.content.Intent intent, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveActivity$1(Intent? intent, int i) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _resolveActivity$1( + reference.pointer, + _id_resolveActivity$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_resolveContentProvider = _class.instanceMethodId( + r'resolveContentProvider', + r'(Ljava/lang/String;Landroid/content/pm/PackageManager$ComponentInfoFlags;)Landroid/content/pm/ProviderInfo;', + ); + + static final _resolveContentProvider = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.ProviderInfo resolveContentProvider(java.lang.String string, android.content.pm.PackageManager$ComponentInfoFlags componentInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveContentProvider( + jni$_.JString? string, + PackageManager$ComponentInfoFlags? componentInfoFlags, + ) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$componentInfoFlags = + componentInfoFlags?.reference ?? jni$_.jNullReference; + return _resolveContentProvider( + reference.pointer, + _id_resolveContentProvider as jni$_.JMethodIDPtr, + _$string.pointer, + _$componentInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_resolveContentProvider$1 = _class.instanceMethodId( + r'resolveContentProvider', + r'(Ljava/lang/String;I)Landroid/content/pm/ProviderInfo;', + ); + + static final _resolveContentProvider$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.ProviderInfo resolveContentProvider(java.lang.String string, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveContentProvider$1(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _resolveContentProvider$1( + reference.pointer, + _id_resolveContentProvider$1 as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_resolveService = _class.instanceMethodId( + r'resolveService', + r'(Landroid/content/Intent;Landroid/content/pm/PackageManager$ResolveInfoFlags;)Landroid/content/pm/ResolveInfo;', + ); + + static final _resolveService = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public android.content.pm.ResolveInfo resolveService(android.content.Intent intent, android.content.pm.PackageManager$ResolveInfoFlags resolveInfoFlags)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveService( + Intent? intent, + PackageManager$ResolveInfoFlags? resolveInfoFlags, + ) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + final _$resolveInfoFlags = + resolveInfoFlags?.reference ?? jni$_.jNullReference; + return _resolveService( + reference.pointer, + _id_resolveService as jni$_.JMethodIDPtr, + _$intent.pointer, + _$resolveInfoFlags.pointer, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_resolveService$1 = _class.instanceMethodId( + r'resolveService', + r'(Landroid/content/Intent;I)Landroid/content/pm/ResolveInfo;', + ); + + static final _resolveService$1 = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract android.content.pm.ResolveInfo resolveService(android.content.Intent intent, int i)` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject? resolveService$1(Intent? intent, int i) { + final _$intent = intent?.reference ?? jni$_.jNullReference; + return _resolveService$1( + reference.pointer, + _id_resolveService$1 as jni$_.JMethodIDPtr, + _$intent.pointer, + i, + ).object(const jni$_.$JObject$NullableType$()); + } + + static final _id_setApplicationCategoryHint = _class.instanceMethodId( + r'setApplicationCategoryHint', + r'(Ljava/lang/String;I)V', + ); + + static final _setApplicationCategoryHint = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public abstract void setApplicationCategoryHint(java.lang.String string, int i)` + void setApplicationCategoryHint(jni$_.JString? string, int i) { + final _$string = string?.reference ?? jni$_.jNullReference; + _setApplicationCategoryHint( + reference.pointer, + _id_setApplicationCategoryHint as jni$_.JMethodIDPtr, + _$string.pointer, + i, + ).check(); + } + + static final _id_setApplicationEnabledSetting = _class.instanceMethodId( + r'setApplicationEnabledSetting', + r'(Ljava/lang/String;II)V', + ); + + static final _setApplicationEnabledSetting = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Int32, jni$_.Int32) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + int, + ) + >(); + + /// from: `public abstract void setApplicationEnabledSetting(java.lang.String string, int i, int i1)` + void setApplicationEnabledSetting(jni$_.JString? string, int i, int i1) { + final _$string = string?.reference ?? jni$_.jNullReference; + _setApplicationEnabledSetting( + reference.pointer, + _id_setApplicationEnabledSetting as jni$_.JMethodIDPtr, + _$string.pointer, + i, + i1, + ).check(); + } + + static final _id_setAutoRevokeWhitelisted = _class.instanceMethodId( + r'setAutoRevokeWhitelisted', + r'(Ljava/lang/String;Z)Z', + ); + + static final _setAutoRevokeWhitelisted = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer, jni$_.Int32)>, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + ) + >(); + + /// from: `public boolean setAutoRevokeWhitelisted(java.lang.String string, boolean z)` + bool setAutoRevokeWhitelisted(jni$_.JString? string, bool z) { + final _$string = string?.reference ?? jni$_.jNullReference; + return _setAutoRevokeWhitelisted( + reference.pointer, + _id_setAutoRevokeWhitelisted as jni$_.JMethodIDPtr, + _$string.pointer, + z ? 1 : 0, + ).boolean; + } + + static final _id_setComponentEnabledSetting = _class.instanceMethodId( + r'setComponentEnabledSetting', + r'(Landroid/content/ComponentName;II)V', + ); + + static final _setComponentEnabledSetting = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Int32, jni$_.Int32) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + int, + int, + ) + >(); + + /// from: `public abstract void setComponentEnabledSetting(android.content.ComponentName componentName, int i, int i1)` + void setComponentEnabledSetting(jni$_.JObject? componentName, int i, int i1) { + final _$componentName = componentName?.reference ?? jni$_.jNullReference; + _setComponentEnabledSetting( + reference.pointer, + _id_setComponentEnabledSetting as jni$_.JMethodIDPtr, + _$componentName.pointer, + i, + i1, + ).check(); + } + + static final _id_setComponentEnabledSettings = _class.instanceMethodId( + r'setComponentEnabledSettings', + r'(Ljava/util/List;)V', + ); + + static final _setComponentEnabledSettings = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void setComponentEnabledSettings(java.util.List list)` + void setComponentEnabledSettings( + jni$_.JList? list, + ) { + final _$list = list?.reference ?? jni$_.jNullReference; + _setComponentEnabledSettings( + reference.pointer, + _id_setComponentEnabledSettings as jni$_.JMethodIDPtr, + _$list.pointer, + ).check(); + } + + static final _id_setInstallerPackageName = _class.instanceMethodId( + r'setInstallerPackageName', + r'(Ljava/lang/String;Ljava/lang/String;)V', + ); + + static final _setInstallerPackageName = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void setInstallerPackageName(java.lang.String string, java.lang.String string1)` + void setInstallerPackageName(jni$_.JString? string, jni$_.JString? string1) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$string1 = string1?.reference ?? jni$_.jNullReference; + _setInstallerPackageName( + reference.pointer, + _id_setInstallerPackageName as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + ).check(); + } + + static final _id_setMimeGroup = _class.instanceMethodId( + r'setMimeGroup', + r'(Ljava/lang/String;Ljava/util/Set;)V', + ); + + static final _setMimeGroup = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + (jni$_.Pointer, jni$_.Pointer) + >, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public void setMimeGroup(java.lang.String string, java.util.Set set)` + void setMimeGroup(jni$_.JString? string, jni$_.JSet? set) { + final _$string = string?.reference ?? jni$_.jNullReference; + final _$set = set?.reference ?? jni$_.jNullReference; + _setMimeGroup( + reference.pointer, + _id_setMimeGroup as jni$_.JMethodIDPtr, + _$string.pointer, + _$set.pointer, + ).check(); + } + + static final _id_updateInstantAppCookie = _class.instanceMethodId( + r'updateInstantAppCookie', + r'([B)V', + ); + + static final _updateInstantAppCookie = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public abstract void updateInstantAppCookie(byte[] bs)` + void updateInstantAppCookie(jni$_.JByteArray? bs) { + final _$bs = bs?.reference ?? jni$_.jNullReference; + _updateInstantAppCookie( + reference.pointer, + _id_updateInstantAppCookie as jni$_.JMethodIDPtr, + _$bs.pointer, + ).check(); + } + + static final _id_verifyPendingInstall = _class.instanceMethodId( + r'verifyPendingInstall', + r'(II)V', + ); + + static final _verifyPendingInstall = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32, jni$_.Int32)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + int, + int, + ) + >(); + + /// from: `public abstract void verifyPendingInstall(int i, int i1)` + void verifyPendingInstall(int i, int i1) { + _verifyPendingInstall( + reference.pointer, + _id_verifyPendingInstall as jni$_.JMethodIDPtr, + i, + i1, + ).check(); + } +} + +final class $PackageManager$NullableType$ extends jni$_.JType { + @jni$_.internal + const $PackageManager$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/pm/PackageManager;'; + + @jni$_.internal + @core$_.override + PackageManager? fromReference(jni$_.JReference reference) => + reference.isNull ? null : PackageManager.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($PackageManager$NullableType$) && + other is $PackageManager$NullableType$; + } +} + +final class $PackageManager$Type$ extends jni$_.JType { + @jni$_.internal + const $PackageManager$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Landroid/content/pm/PackageManager;'; + + @jni$_.internal + @core$_.override + PackageManager fromReference(jni$_.JReference reference) => + PackageManager.fromReference(reference); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $PackageManager$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($PackageManager$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($PackageManager$Type$) && + other is $PackageManager$Type$; + } +} diff --git a/native_interop_demos/permissions_plugin/lib/permissions_plugin.dart b/native_interop_demos/permissions_plugin/lib/permissions_plugin.dart new file mode 100644 index 0000000..56f69ca --- /dev/null +++ b/native_interop_demos/permissions_plugin/lib/permissions_plugin.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; + +import 'permissions_plugin_platform_interface.dart'; +import 'gen/android.g.dart'; +import 'package:jni/jni.dart'; + +class PermissionsPlugin { + Future getPlatformVersion() { + return PermissionsPluginPlatform.instance.getPlatformVersion(); + } + + // + bool checkPermission(JObject context, String permission) { + // Returns a simple true or false if the permission has been granted + var result = ContextCompat.checkSelfPermission( + context, + permission.toJString(), + ); + return result == PackageManager.PERMISSION_GRANTED ? true : false; + } + + int checkAndRequestPermission( + JObject context, + String permission, + Function callback, + ) { + // Do I have permission? + if (ContextCompat.checkSelfPermission(context, permission.toJString()) == + PackageManager.PERMISSION_GRANTED) { + callback(); + } else if (ActivityCompat.shouldShowRequestPermissionRationale( + Jni.androidActivity(PlatformDispatcher.instance.engineId!), + permission.toJString(), + ) == + true) { + // Has the user denied the permission before? + // Give a reason why I need the permission + print("I should ask for permission"); + // TODO Flow to show UI to reshow perms dialog + return -2; + } else { + // Ask for permission + ActivityCompat.requestPermissions( + Jni.androidActivity(PlatformDispatcher.instance.engineId!), + JArray.of(JString.type, [permission.toJString()]), + 0, + ); + } + return 0; + } +} diff --git a/native_interop_demos/permissions_plugin/lib/permissions_plugin_platform_interface.dart b/native_interop_demos/permissions_plugin/lib/permissions_plugin_platform_interface.dart new file mode 100644 index 0000000..6458f69 --- /dev/null +++ b/native_interop_demos/permissions_plugin/lib/permissions_plugin_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'permissions_plugin_method_channel.dart'; + +abstract class PermissionsPluginPlatform extends PlatformInterface { + /// Constructs a PermissionsPluginPlatform. + PermissionsPluginPlatform() : super(token: _token); + + static final Object _token = Object(); + + static PermissionsPluginPlatform _instance = MethodChannelPermissionsPlugin(); + + /// The default instance of [PermissionsPluginPlatform] to use. + /// + /// Defaults to [MethodChannelPermissionsPlugin]. + static PermissionsPluginPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [PermissionsPluginPlatform] when + /// they register themselves. + static set instance(PermissionsPluginPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/native_interop_demos/permissions_plugin/pubspec.yaml b/native_interop_demos/permissions_plugin/pubspec.yaml new file mode 100644 index 0000000..6424545 --- /dev/null +++ b/native_interop_demos/permissions_plugin/pubspec.yaml @@ -0,0 +1,72 @@ +name: permissions_plugin +description: "A new Flutter plugin project." +version: 0.0.1 +homepage: + +environment: + sdk: ^3.10.8 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + jni: ^0.15.2 + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + jnigen: ^0.15.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.example.permissions_plugin + pluginClass: PermissionsPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/native_interop_demos/permissions_plugin/test/permissions_plugin_method_channel_test.dart b/native_interop_demos/permissions_plugin/test/permissions_plugin_method_channel_test.dart new file mode 100644 index 0000000..bf2ba0d --- /dev/null +++ b/native_interop_demos/permissions_plugin/test/permissions_plugin_method_channel_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:permissions_plugin/permissions_plugin_method_channel.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + MethodChannelPermissionsPlugin platform = MethodChannelPermissionsPlugin(); + const MethodChannel channel = MethodChannel('permissions_plugin'); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + (MethodCall methodCall) async { + return '42'; + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + }); + + test('getPlatformVersion', () async { + expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/native_interop_demos/permissions_plugin/test/permissions_plugin_test.dart b/native_interop_demos/permissions_plugin/test/permissions_plugin_test.dart new file mode 100644 index 0000000..f0e5273 --- /dev/null +++ b/native_interop_demos/permissions_plugin/test/permissions_plugin_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:permissions_plugin/permissions_plugin.dart'; +import 'package:permissions_plugin/permissions_plugin_platform_interface.dart'; +import 'package:permissions_plugin/permissions_plugin_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockPermissionsPluginPlatform + with MockPlatformInterfaceMixin + implements PermissionsPluginPlatform { + + @override + Future getPlatformVersion() => Future.value('42'); +} + +void main() { + final PermissionsPluginPlatform initialPlatform = PermissionsPluginPlatform.instance; + + test('$MethodChannelPermissionsPlugin is the default instance', () { + expect(initialPlatform, isInstanceOf()); + }); + + test('getPlatformVersion', () async { + PermissionsPlugin permissionsPlugin = PermissionsPlugin(); + MockPermissionsPluginPlatform fakePlatform = MockPermissionsPluginPlatform(); + PermissionsPluginPlatform.instance = fakePlatform; + + expect(await permissionsPlugin.getPlatformVersion(), '42'); + }); +} diff --git a/native_interop_demos/permissions_plugin/tool/jnigen.dart b/native_interop_demos/permissions_plugin/tool/jnigen.dart new file mode 100644 index 0000000..d177fa0 --- /dev/null +++ b/native_interop_demos/permissions_plugin/tool/jnigen.dart @@ -0,0 +1,35 @@ +import 'dart:io'; + +import 'package:jnigen/jnigen.dart'; + +void main(List args) { + final packageRoot = Platform.script.resolve('../'); + generateJniBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: packageRoot.resolve('lib/gen/android.g.dart'), + structure: OutputStructure.singleFile, + ), + ), + androidSdkConfig: AndroidSdkConfig(addGradleDeps: true, androidExample: 'example/'), + //sourcePath: [packageRoot.resolve('android/src/main/kotlin/com/example/android_launch_activity')], + classes: [ + // provided by Android OS + 'android.app.Application', + 'androidx.activity.ComponentActivity', + 'androidx.fragment.app.FragmentActivity', + 'androidx.activity.result.ActivityResult', + 'androidx.core.app.ActivityCompat', + 'androidx.activity.result.ActivityResultCallback', + 'androidx.activity.result.ActivityResultLauncher', + 'androidx.activity.result.contract.ActivityResultContract', + 'android.content.Intent', + //'android.content.Context', + 'androidx.core.content.ContextCompat', + 'android.Manifest', + 'android.content.pm.PackageManager' + ], + ), + ); +} diff --git a/tool/flutter_ci_script_beta.sh b/tool/flutter_ci_script_beta.sh deleted file mode 100755 index a091de0..0000000 --- a/tool/flutter_ci_script_beta.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -DIR="${BASH_SOURCE%/*}" -source "$DIR/flutter_ci_script_shared.sh" - -flutter doctor -v - -declare -ar PROJECT_NAMES=( - "vertex_ai_firebase_flutter_app" -) - -ci_projects "beta" "${PROJECT_NAMES[@]}" - -echo "-- Success --" diff --git a/tool/flutter_ci_script_master.sh b/tool/flutter_ci_script_master.sh deleted file mode 100755 index ba65a53..0000000 --- a/tool/flutter_ci_script_master.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -DIR="${BASH_SOURCE%/*}" -source "$DIR/flutter_ci_script_shared.sh" - -flutter doctor -v - -declare -ar PROJECT_NAMES=( - "vertex_ai_firebase_flutter_app" -) - -ci_projects "master" "${PROJECT_NAMES[@]}" - -echo "-- Success --" diff --git a/tool/flutter_ci_script_shared.sh b/tool/flutter_ci_script_shared.sh deleted file mode 100644 index f93be22..0000000 --- a/tool/flutter_ci_script_shared.sh +++ /dev/null @@ -1,33 +0,0 @@ -function ci_projects () { - local channel="$1" - - shift - local arr=("$@") - for PROJECT_NAME in "${arr[@]}" - do - echo "== Testing '${PROJECT_NAME}' on Flutter's $channel channel ==" - pushd "${PROJECT_NAME}" - - # Grab packages. - flutter pub get - - # Run the analyzer to find any static analysis issues. - dart analyze --fatal-infos --fatal-warnings - - # Run the formatter on all the dart files to make sure everything's linted. - dart format --output none --set-exit-if-changed . - - # Run the actual tests. - if [ -d "test" ] - then - if grep -q "flutter:" "pubspec.yaml"; then - flutter test - else - # If the project is not a Flutter project, use the Dart CLI. - dart test - fi - fi - - popd - done -} diff --git a/tool/flutter_ci_script_stable.sh b/tool/flutter_ci_script_stable.sh deleted file mode 100755 index e28a400..0000000 --- a/tool/flutter_ci_script_stable.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -DIR="${BASH_SOURCE%/*}" -source "$DIR/flutter_ci_script_shared.sh" - -flutter doctor -v - -declare -ar PROJECT_NAMES=( - "vertex_ai_firebase_flutter_app" -) - -ci_projects "stable" "${PROJECT_NAMES[@]}" - -echo "-- Success --" diff --git a/varfont_shader_puzzle/.gitignore b/varfont_shader_puzzle/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/varfont_shader_puzzle/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +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 +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# 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 diff --git a/varfont_shader_puzzle/.metadata b/varfont_shader_puzzle/.metadata new file mode 100644 index 0000000..620c3fb --- /dev/null +++ b/varfont_shader_puzzle/.metadata @@ -0,0 +1,42 @@ +# 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: "db7ef5bf9f59442b0e200a90587e8fa5e0c6336a" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + - platform: android + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + - platform: ios + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + - platform: linux + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + - platform: macos + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + - platform: windows + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + + # 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' diff --git a/varfont_shader_puzzle/README.md b/varfont_shader_puzzle/README.md new file mode 100644 index 0000000..cec255b --- /dev/null +++ b/varfont_shader_puzzle/README.md @@ -0,0 +1,3 @@ +# Type Jam + +A simple typographically-themed puzzle app to explore creative use of variable fonts and shaders in Flutter. diff --git a/varfont_shader_puzzle/analysis_options.yaml b/varfont_shader_puzzle/analysis_options.yaml new file mode 100644 index 0000000..13d6fe1 --- /dev/null +++ b/varfont_shader_puzzle/analysis_options.yaml @@ -0,0 +1 @@ +include: package:analysis_defaults/flutter.yaml diff --git a/varfont_shader_puzzle/android/.gitignore b/varfont_shader_puzzle/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/varfont_shader_puzzle/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/varfont_shader_puzzle/android/app/build.gradle b/varfont_shader_puzzle/android/app/build.gradle new file mode 100644 index 0000000..665ab67 --- /dev/null +++ b/varfont_shader_puzzle/android/app/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.varfont_shader_puzzle" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.varfont_shader_puzzle" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/varfont_shader_puzzle/android/app/src/debug/AndroidManifest.xml b/varfont_shader_puzzle/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/varfont_shader_puzzle/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/varfont_shader_puzzle/android/app/src/main/AndroidManifest.xml b/varfont_shader_puzzle/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..69cf431 --- /dev/null +++ b/varfont_shader_puzzle/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/varfont_shader_puzzle/android/app/src/main/kotlin/com/example/varfont_shader_puzzle/MainActivity.kt b/varfont_shader_puzzle/android/app/src/main/kotlin/com/example/varfont_shader_puzzle/MainActivity.kt new file mode 100644 index 0000000..ec859b3 --- /dev/null +++ b/varfont_shader_puzzle/android/app/src/main/kotlin/com/example/varfont_shader_puzzle/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.varfont_shader_puzzle + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/varfont_shader_puzzle/android/app/src/main/res/drawable-v21/launch_background.xml b/varfont_shader_puzzle/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/varfont_shader_puzzle/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/varfont_shader_puzzle/android/app/src/main/res/drawable/launch_background.xml b/varfont_shader_puzzle/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/varfont_shader_puzzle/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/varfont_shader_puzzle/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/varfont_shader_puzzle/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/varfont_shader_puzzle/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/varfont_shader_puzzle/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/varfont_shader_puzzle/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/varfont_shader_puzzle/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/varfont_shader_puzzle/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/varfont_shader_puzzle/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/varfont_shader_puzzle/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/varfont_shader_puzzle/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/varfont_shader_puzzle/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/varfont_shader_puzzle/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/varfont_shader_puzzle/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/varfont_shader_puzzle/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/varfont_shader_puzzle/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/varfont_shader_puzzle/android/app/src/main/res/values-night/styles.xml b/varfont_shader_puzzle/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/varfont_shader_puzzle/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/varfont_shader_puzzle/android/app/src/main/res/values/styles.xml b/varfont_shader_puzzle/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/varfont_shader_puzzle/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/varfont_shader_puzzle/android/app/src/profile/AndroidManifest.xml b/varfont_shader_puzzle/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/varfont_shader_puzzle/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/varfont_shader_puzzle/android/build.gradle b/varfont_shader_puzzle/android/build.gradle new file mode 100644 index 0000000..e83fb5d --- /dev/null +++ b/varfont_shader_puzzle/android/build.gradle @@ -0,0 +1,30 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/varfont_shader_puzzle/android/gradle.properties b/varfont_shader_puzzle/android/gradle.properties new file mode 100644 index 0000000..598d13f --- /dev/null +++ b/varfont_shader_puzzle/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/varfont_shader_puzzle/android/gradle/wrapper/gradle-wrapper.properties b/varfont_shader_puzzle/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/varfont_shader_puzzle/android/gradle/wrapper/gradle-wrapper.properties @@ -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-7.5-all.zip diff --git a/varfont_shader_puzzle/android/settings.gradle b/varfont_shader_puzzle/android/settings.gradle new file mode 100644 index 0000000..7cd7128 --- /dev/null +++ b/varfont_shader_puzzle/android/settings.gradle @@ -0,0 +1,29 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + + plugins { + id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false +} + +include ":app" diff --git a/varfont_shader_puzzle/assets/fonts/Amstelvar-Roman[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,wdth,wght,opsz].ttf b/varfont_shader_puzzle/assets/fonts/Amstelvar-Roman[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,wdth,wght,opsz].ttf new file mode 100644 index 0000000..062b4a4 Binary files /dev/null and b/varfont_shader_puzzle/assets/fonts/Amstelvar-Roman[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,wdth,wght,opsz].ttf differ diff --git a/varfont_shader_puzzle/assets/fonts/Roboto-Bold.ttf b/varfont_shader_puzzle/assets/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..43da14d Binary files /dev/null and b/varfont_shader_puzzle/assets/fonts/Roboto-Bold.ttf differ diff --git a/varfont_shader_puzzle/assets/fonts/Roboto-Regular.ttf b/varfont_shader_puzzle/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..ddf4bfa Binary files /dev/null and b/varfont_shader_puzzle/assets/fonts/Roboto-Regular.ttf differ diff --git a/varfont_shader_puzzle/assets/images/specimen-1-glitch.png b/varfont_shader_puzzle/assets/images/specimen-1-glitch.png new file mode 100644 index 0000000..3e4f2bb Binary files /dev/null and b/varfont_shader_puzzle/assets/images/specimen-1-glitch.png differ diff --git a/varfont_shader_puzzle/assets/images/specimen-1.png b/varfont_shader_puzzle/assets/images/specimen-1.png new file mode 100644 index 0000000..9914ed9 Binary files /dev/null and b/varfont_shader_puzzle/assets/images/specimen-1.png differ diff --git a/varfont_shader_puzzle/assets/images/specimen-2.png b/varfont_shader_puzzle/assets/images/specimen-2.png new file mode 100644 index 0000000..01682fb Binary files /dev/null and b/varfont_shader_puzzle/assets/images/specimen-2.png differ diff --git a/varfont_shader_puzzle/assets/images/wallpaper1.png b/varfont_shader_puzzle/assets/images/wallpaper1.png new file mode 100644 index 0000000..b7d1850 Binary files /dev/null and b/varfont_shader_puzzle/assets/images/wallpaper1.png differ diff --git a/varfont_shader_puzzle/assets/images/wallpaper2.png b/varfont_shader_puzzle/assets/images/wallpaper2.png new file mode 100644 index 0000000..01c4827 Binary files /dev/null and b/varfont_shader_puzzle/assets/images/wallpaper2.png differ diff --git a/varfont_shader_puzzle/assets/images/wallpaper3.png b/varfont_shader_puzzle/assets/images/wallpaper3.png new file mode 100644 index 0000000..6f4af43 Binary files /dev/null and b/varfont_shader_puzzle/assets/images/wallpaper3.png differ diff --git a/varfont_shader_puzzle/codelab_rebuild.yaml b/varfont_shader_puzzle/codelab_rebuild.yaml new file mode 100644 index 0000000..46a1ecc --- /dev/null +++ b/varfont_shader_puzzle/codelab_rebuild.yaml @@ -0,0 +1,16 @@ +# Run with tooling from https://github.com/flutter/codelabs/tree/main/tooling/codelab_rebuild +name: Animations rebuild script +steps: + - name: Remove runners + rmdirs: + - android + - ios + - linux + - macos + - windows + - name: Flutter recreate + flutter: create . --platforms android,ios,linux,macos,windows + - name: Build for iOS + flutter: build ios --simulator + - name: Build for macOS + flutter: build macos diff --git a/varfont_shader_puzzle/ios/.gitignore b/varfont_shader_puzzle/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/varfont_shader_puzzle/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +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 diff --git a/varfont_shader_puzzle/ios/Flutter/AppFrameworkInfo.plist b/varfont_shader_puzzle/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/varfont_shader_puzzle/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/varfont_shader_puzzle/ios/Flutter/Debug.xcconfig b/varfont_shader_puzzle/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/varfont_shader_puzzle/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/varfont_shader_puzzle/ios/Flutter/Release.xcconfig b/varfont_shader_puzzle/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/varfont_shader_puzzle/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/varfont_shader_puzzle/ios/Podfile b/varfont_shader_puzzle/ios/Podfile new file mode 100644 index 0000000..fdcc671 --- /dev/null +++ b/varfont_shader_puzzle/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.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! + use_modular_headers! + + 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 diff --git a/varfont_shader_puzzle/ios/Runner.xcodeproj/project.pbxproj b/varfont_shader_puzzle/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4cf5223 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,722 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2D29E31A3A79F9333791B51D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73A94518C1B077566B730F17 /* Pods_Runner.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 891BD033C601B72D562DF8C0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6933D4A3C5277B69ACBFB6E /* Pods_RunnerTests.framework */; }; + 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 */ + 0D9FDBAE730D44B5DAF66533 /* 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 = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1F18FCED1ECA53679E854763 /* 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 43CE1627606473C19FB03FF3 /* 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 = ""; }; + 517D85BC067A0EA0E96EA4A8 /* 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 = ""; }; + 6364E8B2E7C9C391682E8BCF /* 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 = ""; }; + 73A94518C1B077566B730F17 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C6933D4A3C5277B69ACBFB6E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F9EB5298186B85BCAD585118 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2D29E31A3A79F9333791B51D /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DF1CD5C6DFB4874767236D73 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 891BD033C601B72D562DF8C0 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 07DA3946E7E7EFEFFAD0000F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 73A94518C1B077566B730F17 /* Pods_Runner.framework */, + C6933D4A3C5277B69ACBFB6E /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 2D4C743D47EF854290F46B78 /* Pods */ = { + isa = PBXGroup; + children = ( + F9EB5298186B85BCAD585118 /* Pods-Runner.debug.xcconfig */, + 1F18FCED1ECA53679E854763 /* Pods-Runner.release.xcconfig */, + 6364E8B2E7C9C391682E8BCF /* Pods-Runner.profile.xcconfig */, + 0D9FDBAE730D44B5DAF66533 /* Pods-RunnerTests.debug.xcconfig */, + 43CE1627606473C19FB03FF3 /* Pods-RunnerTests.release.xcconfig */, + 517D85BC067A0EA0E96EA4A8 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 2D4C743D47EF854290F46B78 /* Pods */, + 07DA3946E7E7EFEFFAD0000F /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 1CC0CE02DDD22E3C34E46370 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + DF1CD5C6DFB4874767236D73 /* 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 = ( + 8C070EA05DFDB24F15A12816 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 3F8C0B811F28285ABC292845 /* [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 = 1430; + 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 = ( + ); + 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 */ + 1CC0CE02DDD22E3C34E46370 /* [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"; + }; + 3F8C0B811F28285ABC292845 /* [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; + }; + 8C070EA05DFDB24F15A12816 /* [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; + }; + 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"; + }; +/* 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 */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + 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 = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0D9FDBAE730D44B5DAF66533 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 43CE1627606473C19FB03FF3 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 517D85BC067A0EA0E96EA4A8 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + 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 = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + 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 = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */; +} diff --git a/varfont_shader_puzzle/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/varfont_shader_puzzle/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/varfont_shader_puzzle/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/varfont_shader_puzzle/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/varfont_shader_puzzle/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/varfont_shader_puzzle/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/varfont_shader_puzzle/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/varfont_shader_puzzle/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..87131a0 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/varfont_shader_puzzle/ios/Runner.xcworkspace/contents.xcworkspacedata b/varfont_shader_puzzle/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/varfont_shader_puzzle/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/varfont_shader_puzzle/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/varfont_shader_puzzle/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/varfont_shader_puzzle/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/varfont_shader_puzzle/ios/Runner/AppDelegate.swift b/varfont_shader_puzzle/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/varfont_shader_puzzle/ios/Runner/Base.lproj/LaunchScreen.storyboard b/varfont_shader_puzzle/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/varfont_shader_puzzle/ios/Runner/Base.lproj/Main.storyboard b/varfont_shader_puzzle/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/varfont_shader_puzzle/ios/Runner/Info.plist b/varfont_shader_puzzle/ios/Runner/Info.plist new file mode 100644 index 0000000..1028bad --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Varfont Shader Puzzle + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + varfont_shader_puzzle + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/varfont_shader_puzzle/ios/Runner/Runner-Bridging-Header.h b/varfont_shader_puzzle/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/varfont_shader_puzzle/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/varfont_shader_puzzle/ios/RunnerTests/RunnerTests.swift b/varfont_shader_puzzle/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/varfont_shader_puzzle/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/varfont_shader_puzzle/lib/components/components.dart b/varfont_shader_puzzle/lib/components/components.dart new file mode 100644 index 0000000..f6974e9 --- /dev/null +++ b/varfont_shader_puzzle/lib/components/components.dart @@ -0,0 +1,9 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'fragment_shaded.dart'; +export 'lightboxed_panel.dart'; +export 'rotator_puzzle.dart'; +export 'wonky_anim_palette.dart'; +export 'wonky_char.dart'; diff --git a/varfont_shader_puzzle/lib/components/fragment_shaded.dart b/varfont_shader_puzzle/lib/components/fragment_shaded.dart new file mode 100644 index 0000000..80b74aa --- /dev/null +++ b/varfont_shader_puzzle/lib/components/fragment_shaded.dart @@ -0,0 +1,282 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +enum Shader { + nothing('nothing'), + bwSplit('bw_split'), + colorSplit('color_split'), + rowOffset('row_offset'), + wavyCirc('wavy_circ'), + wavy('wavy'), + wavy2('wavy2'); + + const Shader(this.name); + final String name; + Future get program => + ui.FragmentProgram.fromAsset('shaders/$name.frag'); +} + +class FragmentShaded extends StatefulWidget { + final Widget child; + final Shader shader; + final int shaderDuration; + static const int dampenDuration = 1000; + static final Map _programCache = {}; + + const FragmentShaded({ + required this.shader, + required this.shaderDuration, + required this.child, + super.key, + }); + + @override + State createState() => FragmentShadedState(); +} + +class FragmentShadedState extends State + with TickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _dampenAnimation; + late final Animation _dampenCurve; + late final AnimationController _dampenController; + late AnimatingSamplerBuilder builder; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: widget.shaderDuration), + )..repeat(reverse: false); + _dampenController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: FragmentShaded.dampenDuration), + ); + _dampenCurve = CurvedAnimation( + parent: _dampenController, + curve: Curves.easeInOut, + ); + _dampenAnimation = Tween( + begin: 1.0, + end: 0.0, + ).animate(_dampenCurve); + initializeFragmentProgramsAndBuilder(); + } + + Future initializeFragmentProgramsAndBuilder() async { + if (FragmentShaded._programCache.isEmpty) { + for (final shader in Shader.values) { + FragmentShaded._programCache[shader] = await shader.program; + } + } + + setState(() { + builder = AnimatingSamplerBuilder( + _controller, + _dampenAnimation, + FragmentShaded._programCache[widget.shader]!.fragmentShader(), + ); + }); + } + + @override + void dispose() { + _controller.dispose(); + _dampenController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (null == FragmentShaded._programCache[widget.shader]) { + setState(() {}); + return const SizedBox(width: 0, height: 0); + } + return Transform.scale( + scale: 0.5, + child: ShaderSamplerBuilder(builder, child: widget.child), + ); + } + + void startDampening() { + _dampenController.forward(); + } +} + +class AnimatingSamplerBuilder extends SamplerBuilder { + AnimatingSamplerBuilder( + this.animation, + this.dampenAnimation, + this.fragmentShader, + ) { + animation.addListener(notifyListeners); + dampenAnimation.addListener(notifyListeners); + } + + final Animation animation; + final Animation dampenAnimation; + + final ui.FragmentShader fragmentShader; + + @override + void paint(ui.Image image, Size size, ui.Canvas canvas) { + // animation + fragmentShader.setFloat(0, animation.value); + // width + fragmentShader.setFloat(1, size.width); + // height + fragmentShader.setFloat(2, size.height); + // dampener + fragmentShader.setFloat(3, dampenAnimation.value); + // sampler + fragmentShader.setImageSampler(0, image); + + canvas.drawRect(Offset.zero & size, Paint()..shader = fragmentShader); + } +} + +abstract class SamplerBuilder extends ChangeNotifier { + void paint(ui.Image image, Size size, ui.Canvas canvas); +} + +class ShaderSamplerBuilder extends StatelessWidget { + const ShaderSamplerBuilder(this.builder, {required this.child, super.key}); + + final SamplerBuilder builder; + final Widget child; + + @override + Widget build(BuildContext context) { + return RepaintBoundary(child: _ShaderSamplerImpl(builder, child: child)); + } +} + +class _ShaderSamplerImpl extends SingleChildRenderObjectWidget { + const _ShaderSamplerImpl(this.builder, {super.child}); + + final SamplerBuilder builder; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderShaderSamplerBuilderWidget( + devicePixelRatio: MediaQuery.of(context).devicePixelRatio, + builder: builder, + ); + } + + @override + void updateRenderObject( + BuildContext context, + covariant RenderObject renderObject, + ) { + (renderObject as _RenderShaderSamplerBuilderWidget) + ..devicePixelRatio = MediaQuery.of(context).devicePixelRatio + ..builder = builder; + } +} + +// A render object that conditionally converts its child into a [ui.Image] +// and then paints it in place of the child. +class _RenderShaderSamplerBuilderWidget extends RenderProxyBox { + // Create a new [_RenderSnapshotWidget]. + _RenderShaderSamplerBuilderWidget({ + required double devicePixelRatio, + required SamplerBuilder builder, + }) : _devicePixelRatio = devicePixelRatio, + _builder = builder; + + /// The device pixel ratio used to create the child image. + double get devicePixelRatio => _devicePixelRatio; + double _devicePixelRatio; + set devicePixelRatio(double value) { + if (value == devicePixelRatio) { + return; + } + _devicePixelRatio = value; + if (_childRaster == null) { + return; + } else { + _childRaster?.dispose(); + _childRaster = null; + markNeedsPaint(); + } + } + + /// The painter used to paint the child snapshot or child widgets. + SamplerBuilder get builder => _builder; + SamplerBuilder _builder; + set builder(SamplerBuilder value) { + if (value == builder) { + return; + } + builder.removeListener(markNeedsPaint); + _builder = value; + builder.addListener(markNeedsPaint); + markNeedsPaint(); + } + + ui.Image? _childRaster; + + @override + void attach(PipelineOwner owner) { + builder.addListener(markNeedsPaint); + super.attach(owner); + } + + @override + void detach() { + _childRaster?.dispose(); + _childRaster = null; + builder.removeListener(markNeedsPaint); + super.detach(); + } + + @override + void dispose() { + builder.removeListener(markNeedsPaint); + _childRaster?.dispose(); + _childRaster = null; + super.dispose(); + } + + // Paint [child] with this painting context, then convert to a raster and detach all + // children from this layer. + ui.Image? _paintAndDetachToImage() { + final OffsetLayer offsetLayer = OffsetLayer(); + final PaintingContext context = PaintingContext( + offsetLayer, + Offset.zero & size, + ); + super.paint(context, Offset.zero); + // This ignore is here because this method is protected by the `PaintingContext`. Adding a new + // method that performs the work of `_paintAndDetachToImage` would avoid the need for this, but + // that would conflict with our goals of minimizing painting context. + // ignore: invalid_use_of_protected_member + context.stopRecordingIfNeeded(); + final ui.Image image = offsetLayer.toImageSync( + Offset.zero & size, + pixelRatio: devicePixelRatio, + ); + offsetLayer.dispose(); + return image; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (size.isEmpty) { + _childRaster?.dispose(); + _childRaster = null; + return; + } + _childRaster?.dispose(); + _childRaster = _paintAndDetachToImage(); + builder.paint(_childRaster!, size, context.canvas); + } +} diff --git a/varfont_shader_puzzle/lib/components/lightboxed_panel.dart b/varfont_shader_puzzle/lib/components/lightboxed_panel.dart new file mode 100644 index 0000000..2e99e93 --- /dev/null +++ b/varfont_shader_puzzle/lib/components/lightboxed_panel.dart @@ -0,0 +1,140 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import '../page_content/pages_flow.dart'; +import '../styles.dart'; + +class LightboxedPanel extends StatefulWidget { + final PageConfig pageConfig; + final List content; + final double width = 300; + final Function? onDismiss; + final bool fadeOnDismiss; + final int? autoDismissAfter; + final bool buildButton; + final Color lightBoxBgColor; + final Color cardBgColor; + + const LightboxedPanel({ + super.key, + required this.pageConfig, + required this.content, + this.onDismiss, + this.fadeOnDismiss = true, + this.autoDismissAfter, + this.buildButton = true, + this.lightBoxBgColor = const Color.fromARGB(200, 255, 255, 255), + this.cardBgColor = Colors.white, + }); + + @override + State createState() => _LightboxedPanelState(); +} + +class _LightboxedPanelState extends State { + bool _fading = false; + bool _show = true; + late int _fadeOutDur = 200; + + @override + void initState() { + _fadeOutDur = widget.fadeOnDismiss ? _fadeOutDur : 0; + if (null != widget.autoDismissAfter) { + _fadeOutDur = 0; + Future.delayed( + Duration(milliseconds: widget.autoDismissAfter!), + handleDismiss, + ); + } + super.initState(); + } + + void handleDismiss() { + if (widget.fadeOnDismiss) { + setState(() { + _fading = true; + }); + } + Future.delayed(Duration(milliseconds: _fadeOutDur), () { + setState(() { + if (widget.fadeOnDismiss) { + _show = false; + } + if (null != widget.onDismiss) { + widget.onDismiss!(); + } + }); + }); + } + + List buttonComponents() { + return [ + Column( + children: [ + const SizedBox(height: 8), + TextButton( + onPressed: handleDismiss, + style: ButtonStyles.style(), + child: Text( + 'OK', + style: TextStyles.bodyStyle().copyWith( + color: Colors.white, + height: 1.2, + ), + ), + ), + ], + ), + ]; + } + + @override + Widget build(BuildContext context) { + if (_show) { + return AnimatedOpacity( + opacity: _fading ? 0 : 1, + curve: Curves.easeOut, + duration: Duration(milliseconds: _fadeOutDur), + child: DecoratedBox( + decoration: BoxDecoration(color: widget.lightBoxBgColor), + child: Center( + child: SizedBox( + width: widget.width, + child: DecoratedBox( + decoration: BoxDecoration( + color: widget.cardBgColor, + border: Border.all( + color: const Color.fromARGB(255, 200, 200, 200), + width: 1.0, + ), + boxShadow: const [ + BoxShadow( + color: Color.fromARGB(30, 0, 0, 0), + offset: Offset.zero, + blurRadius: 4.0, + spreadRadius: 2.0, + ), + ], + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: + widget.content + + (widget.buildButton ? buttonComponents() : []), + ), + ), + ), + ), + ), + ), + ); + } + return const SizedBox(width: 0, height: 0); + } +} diff --git a/varfont_shader_puzzle/lib/components/rotator_puzzle.dart b/varfont_shader_puzzle/lib/components/rotator_puzzle.dart new file mode 100644 index 0000000..f84ad4a --- /dev/null +++ b/varfont_shader_puzzle/lib/components/rotator_puzzle.dart @@ -0,0 +1,431 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../model/puzzle_model.dart'; +import '../page_content/pages_flow.dart'; +import 'components.dart'; + +class RotatorPuzzle extends StatefulWidget { + final PageConfig pageConfig; + final int numTiles; + final int puzzleNum; + final Shader shader; + final int shaderDuration; + + final String tileShadedString; + final double tileShadedStringSize; + final EdgeInsets tileShadedStringPadding; + final int tileShadedStringAnimDuration; + final List tileShadedStringAnimSettings; + final double tileScaleModifier; + + const RotatorPuzzle({ + super.key, + required this.pageConfig, + required this.numTiles, + required this.puzzleNum, + required this.shader, + required this.shaderDuration, + required this.tileShadedString, + required this.tileShadedStringSize, + required this.tileShadedStringPadding, + required this.tileShadedStringAnimDuration, + this.tileShadedStringAnimSettings = const [], + this.tileScaleModifier = 1.0, + }); + + @override + State createState() => RotatorPuzzleState(); +} + +class RotatorPuzzleState extends State + with TickerProviderStateMixin { + late PuzzleModel puzzleModel; + bool solved = false; + late final AnimationController animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1000), + ); + late final CurvedAnimation animationCurve = CurvedAnimation( + parent: animationController, + curve: const Interval(0.2, 0.45, curve: Curves.easeOut), + ); + late Animation opacAnimation = Tween( + begin: 0.4, + end: 1.0, + ).animate(animationCurve)..addListener(() { + setState(() {}); + }); + + List> tileKeys = []; + GlobalKey shadedWidgetStackHackStateKey = GlobalKey(); + GlobalKey shadedWidgetRepaintBoundaryKey = GlobalKey(); + GlobalKey tileBgWonkyCharKey = GlobalKey(); + ui.Image? shadedImg; + + @override + void initState() { + for (int i = 0; i < widget.numTiles; i++) { + tileKeys.add(GlobalKey()); + } + puzzleModel = PuzzleModel( + dim: widget.numTiles, + ); //TODO check if correct; correlate dim and numTiles; probably get rid of numTiles + generateTiles(); + shuffle(); + super.initState(); + } + + List generateTiles() { + // TODO move to build? + List tiles = []; + int dim = sqrt(widget.numTiles).round(); + for (int i = 0; i < widget.numTiles; i++) { + RotatorPuzzleTile tile = RotatorPuzzleTile( + key: tileKeys[i], + tileID: i, + row: (i / dim).floor(), + col: i % dim, + parentState: this, + shader: widget.shader, + shaderDuration: widget.shaderDuration, + tileShadedString: widget.tileShadedString, + tileShadedStringSize: widget.tileShadedStringSize, + tileShadedStringPadding: widget.tileShadedStringPadding, + animationSettings: widget.tileShadedStringAnimSettings, + tileShadedStringAnimDuration: widget.tileShadedStringAnimDuration, + tileScaleModifier: widget.tileScaleModifier, + ); + tiles.add(tile); + } + return tiles; + } + + void handlePointerDown({required int tileID}) { + puzzleModel.rotateTile(tileID); + if (puzzleModel.allRotationsCorrect()) { + handleSolved(); + } + } + + void handleSolved() { + animationController.addStatusListener((status) { + solved = true; + for (GlobalKey k in tileKeys) { + if (null != k.currentState && k.currentState!.mounted) { + startDampening(); + tileBgWonkyCharKey.currentState!.stopAnimation(); + } + } + if (status == AnimationStatus.completed) { + Future.delayed( + const Duration(milliseconds: FragmentShaded.dampenDuration + 250), + () { + widget.pageConfig.pageController.nextPage( + duration: const Duration( + milliseconds: PagesFlow.pageScrollDuration, + ), + curve: Curves.easeOut, + ); + }, + ); + } + }); + animationController.forward(); + } + + void shuffle() { + Random rng = Random(0xC00010FF); + for (int i = 0; i < widget.numTiles; i++) { + int rando = rng.nextInt(3); + puzzleModel.setTileStatus(i, rando); + if (puzzleModel.allRotationsCorrect()) { + // fallback to prevent starting on solved puzzle + puzzleModel.setTileStatus(0, 1); + } + } + } + + double tileSize() { + return widget.pageConfig.puzzleSize / sqrt(widget.numTiles); + } + + List tileCoords({required int row, required int col}) { + return [col * tileSize(), row * tileSize()]; + } + + void setImageFromRepaintBoundary(GlobalKey which) { + final BuildContext? context = which.currentContext; + if (null != context) { + final RenderRepaintBoundary boundary = + context.findRenderObject()! as RenderRepaintBoundary; + final ui.Image img = boundary.toImageSync(); + if (mounted) { + setState(() { + shadedImg = img; + }); + } + } + } + + void startDampening() { + if (null != shadedWidgetStackHackStateKey.currentState && + shadedWidgetStackHackStateKey.currentState!.mounted) { + shadedWidgetStackHackStateKey.currentState!.startDampening(); + } + } + + @override + Widget build(BuildContext context) { + // TODO fix widget implementation to remove the need for this hack + // to force a setState rebuild + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); + // end hack ---------------- + setImageFromRepaintBoundary(shadedWidgetRepaintBoundaryKey); + return Center( + child: SizedBox( + width: widget.pageConfig.puzzleSize, + height: widget.pageConfig.puzzleSize, + child: Opacity( + opacity: opacAnimation.value, + child: Stack( + children: + [ + Positioned( + left: -9999, + top: -9999, + child: RepaintBoundary( + key: shadedWidgetRepaintBoundaryKey, + child: SizedBox( + width: widget.pageConfig.puzzleSize * 4, + height: widget.pageConfig.puzzleSize * 4, + child: Center( + child: FragmentShaded( + key: shadedWidgetStackHackStateKey, + shader: widget.shader, + shaderDuration: widget.shaderDuration, + child: Padding( + padding: widget.tileShadedStringPadding, + child: WonkyChar( + key: tileBgWonkyCharKey, + text: widget.tileShadedString, + size: widget.tileShadedStringSize, + animDurationMillis: + widget.tileShadedStringAnimDuration, + animationSettings: + widget.tileShadedStringAnimSettings, + ), + ), + ), + ), + ), + ), + ), + ] + + generateTiles(), + ), + ), + ), + ); + } +} + +//////////////////////////////////////////////////////// + +class RotatorPuzzleTile extends StatefulWidget { + final int tileID; + final RotatorPuzzleState parentState; + final Shader shader; + final int shaderDuration; + final String tileShadedString; + final double tileShadedStringSize; + final EdgeInsets tileShadedStringPadding; + final int tileShadedStringAnimDuration; + final List animationSettings; + final double tileScaleModifier; + + // TODO get row/col out into model + final int row; + final int col; + + RotatorPuzzleTile({ + super.key, + required this.tileID, + required this.row, + required this.col, + required this.parentState, + required this.shader, + required this.shaderDuration, + required this.tileShadedString, + required this.tileShadedStringSize, + required this.tileShadedStringPadding, + required this.animationSettings, + required this.tileShadedStringAnimDuration, + required this.tileScaleModifier, + }); + + final State tileState = RotatorPuzzleTileState(); + + @override + State createState() => RotatorPuzzleTileState(); +} + +class RotatorPuzzleTileState extends State + with TickerProviderStateMixin { + double touchedOpac = 0.0; + Duration touchedOpacDur = const Duration(milliseconds: 50); + late final AnimationController animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + late final CurvedAnimation animationCurve = CurvedAnimation( + parent: animationController, + curve: Curves.easeOut, + ); + late Animation animation; + + @override + void initState() { + super.initState(); + animation = Tween( + // initialize animation to starting point + begin: currentStatus() * pi * 0.5, + end: currentStatus() * pi * 0.5, + ).animate(animationController); + } + + @override + Widget build(BuildContext context) { + // TODO fix widget implementation to remove the need for this hack + // to force a setState rebuild + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); + // end hack ------------------------------ + List coords = widget.parentState.tileCoords( + row: widget.row, + col: widget.col, + ); + double zeroPoint = + widget.parentState.widget.pageConfig.puzzleSize * .5 - + widget.parentState.tileSize() * 0.5; + + return Stack( + children: [ + Stack( + children: [ + Positioned( + left: coords[0], + top: coords[1], + child: Transform( + transform: Matrix4.rotationZ(animation.value), + alignment: Alignment.center, + child: GestureDetector( + onTap: handlePointerDown, + child: ClipRect( + child: SizedBox( + width: widget.parentState.tileSize(), + height: widget.parentState.tileSize(), + child: OverflowBox( + maxHeight: + widget.parentState.widget.pageConfig.puzzleSize, + maxWidth: + widget.parentState.widget.pageConfig.puzzleSize, + child: Transform.translate( + offset: Offset( + zeroPoint - + widget.col * widget.parentState.tileSize(), + zeroPoint - + widget.row * widget.parentState.tileSize(), + ), + child: SizedBox( + width: + widget.parentState.widget.pageConfig.puzzleSize, + height: + widget.parentState.widget.pageConfig.puzzleSize, + child: Transform.scale( + scale: widget.tileScaleModifier, + child: RawImage( + image: widget.parentState.shadedImg, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + // puzzle tile overlay fades in/out on tap, to indicate touched tile + Positioned( + left: coords[0], + top: coords[1], + child: IgnorePointer( + child: AnimatedOpacity( + opacity: touchedOpac, + duration: touchedOpacDur, + onEnd: () { + if (touchedOpac == 1.0) { + touchedOpac = 0.0; + touchedOpacDur = const Duration(milliseconds: 300); + setState(() {}); + } + }, + child: DecoratedBox( + decoration: const BoxDecoration( + color: Color.fromARGB(120, 0, 0, 0), + ), + child: SizedBox( + width: widget.parentState.tileSize(), + height: widget.parentState.tileSize(), + ), + ), + ), + ), + ), + ], + ), + ], + ); + } + + void handlePointerDown() { + if (!widget.parentState.solved) { + int oldStatus = currentStatus(); + widget.parentState.handlePointerDown(tileID: widget.tileID); + touchedOpac = 1.0; + touchedOpacDur = const Duration(milliseconds: 100); + rotateTile(oldStatus: oldStatus); + setState(() {}); + } + } + + int currentStatus() { + return widget.parentState.puzzleModel.getTileStatus(widget.tileID); + } + + void rotateTile({required int oldStatus}) { + animation = Tween( + begin: oldStatus * pi * 0.5, + end: currentStatus() * pi * 0.5, + ).animate(animationController)..addListener(() { + setState(() {}); + }); + animationController.reset(); + animationController.forward(); + } +} diff --git a/varfont_shader_puzzle/lib/components/wonky_anim_palette.dart b/varfont_shader_puzzle/lib/components/wonky_anim_palette.dart new file mode 100644 index 0000000..a3e2acb --- /dev/null +++ b/varfont_shader_puzzle/lib/components/wonky_anim_palette.dart @@ -0,0 +1,332 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; +import 'package:flutter/material.dart'; +import '../components/components.dart'; + +// WonkyAnimPalette class is meant to be used with WonkyChar +// to create animations based on variable font settings (aka 'axes'), +// and a few basic settings like scale, rotation, etc. +// The choice of variable font axes to implement in this class and +// default min/max values for variable font axes are hard-coded +// for Amstelvar font, packaged and used in this project. +// Other variable fonts will have different available axes and min/max values. +// +// See articles on variable fonts at https://fonts.google.com/knowledge/topics/variable_fonts +// See a list of variable fonts in the Google Fonts lineup, along with +// an enumeration of variable font axes at https://fonts.google.com/variablefonts + +class WonkyAnimPalette { + const WonkyAnimPalette({Key? key}); + static const Curve defaultCurve = Curves.easeInOut; + + // basic (settings unrelated to variable font) + static WonkyAnimSetting scale({ + double from = 1, + double to = 2, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'basic', + property: 'scale', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting offsetX({ + double from = -50, + double to = 50, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'basic', + property: 'offsetX', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting offsetY({ + double from = -50, + double to = 50, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'basic', + property: 'offsetY', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting rotation({ + double from = -pi, + double to = pi, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'basic', + property: 'rotation', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting color({ + Color from = Colors.blue, + Color to = Colors.red, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'basic', + property: 'color', + fromTo: RangeColor(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + // font variants (variable font settings) + static WonkyAnimSetting opticalSize({ + double from = 8, + double to = 144, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'opsz', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting weight({ + double from = 100, + double to = 1000, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'wght', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting grade({ + double from = -300, + double to = 500, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'GRAD', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting slant({ + double from = -10, + double to = 0, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'slnt', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting width({ + double from = 50, + double to = 125, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'wdth', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting thickStroke({ + double from = 18, + double to = 263, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'XOPQ', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting thinStroke({ + double from = 15, + double to = 132, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'YOPQ', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting counterWd({ + double from = 324, + double to = 640, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'XTRA', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting upperCaseHt({ + double from = 500, + double to = 1000, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'YTUC', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting lowerCaseHt({ + double from = 420, + double to = 570, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'YTLC', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting ascenderHt({ + double from = 500, + double to = 983, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'YTAS', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting descenderDepth({ + double from = -500, + double to = -138, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'YTDE', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } + + static WonkyAnimSetting figureHt({ + double from = 425, + double to = 1000, + double startAt = 0, + double endAt = 1, + Curve curve = defaultCurve, + }) { + return WonkyAnimSetting( + type: 'fv', + property: 'YTFI', + fromTo: RangeDbl(from: from, to: to), + startAt: startAt, + endAt: endAt, + curve: curve, + ); + } +} diff --git a/varfont_shader_puzzle/lib/components/wonky_char.dart b/varfont_shader_puzzle/lib/components/wonky_char.dart new file mode 100644 index 0000000..399dbef --- /dev/null +++ b/varfont_shader_puzzle/lib/components/wonky_char.dart @@ -0,0 +1,242 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter/material.dart'; + +class WonkyChar extends StatefulWidget { + final String text; + final double size; + final double baseRotation; + final int animDurationMillis; + final List animationSettings; + const WonkyChar({ + super.key, + required this.text, + required this.size, + this.baseRotation = 0, + this.animDurationMillis = 1000, + this.animationSettings = const [], + }); + + @override + State createState() => WonkyCharState(); +} + +class WonkyCharState extends State + with SingleTickerProviderStateMixin { + bool loopingAnimation = true; + late AnimationController _animController; + final List> _curves = []; + late final List _fvAnimations = []; + final List _fvAxes = []; + // default curve and animations in case user sets nothing for them + late final defaultCurve = CurvedAnimation( + parent: _animController, + curve: const Interval(0, 1, curve: Curves.linear), + ); + late Animation _scaleAnimation = Tween( + begin: 1, + end: 1, + ).animate(defaultCurve); + late Animation _offsetXAnimation = Tween( + begin: 0, + end: 0, + ).animate(defaultCurve); + late Animation _offsetYAnimation = Tween( + begin: 0, + end: 0, + ).animate(defaultCurve); + late Animation _rotationAnimation = Tween( + begin: 0, + end: 0, + ).animate(defaultCurve); + late Animation _colorAnimation = ColorTween( + begin: Colors.black, + end: Colors.black, + ).animate(defaultCurve); + + @override + void initState() { + super.initState(); + initAnimations(widget.animationSettings); + _animController + ..addListener(() { + setState(() {}); + }) + ..addStatusListener((status) { + if (status == AnimationStatus.completed && loopingAnimation) { + _animController.reverse(); + } else if (status == AnimationStatus.dismissed && loopingAnimation) { + _animController.forward(); + } + }); + _animController.forward(); + } + + @override + void dispose() { + _animController.dispose(); + super.dispose(); + } + + void stopAnimation() { + _animController.stop(); + } + + @override + Widget build(BuildContext context) { + List fontVariations = []; + for (int i = 0; i < _fvAxes.length; i++) { + fontVariations.add( + ui.FontVariation(_fvAxes[i], _fvAnimations[i].value as double), + ); + } + return Transform( + alignment: Alignment.center, + transform: + Matrix4.translationValues( + _offsetXAnimation.value as double, + _offsetYAnimation.value as double, + 0, + ) + ..scale(_scaleAnimation.value) + ..rotateZ( + widget.baseRotation + (_rotationAnimation.value as double), + ), + child: IgnorePointer( + child: Text( + widget.text, + textAlign: TextAlign.center, + style: TextStyle( + color: _colorAnimation.value as Color?, + fontFamily: 'Amstelvar', + fontSize: widget.size, + fontVariations: fontVariations, + ), + ), + ), + ); + } + + void initAnimations(List settings) { + _animController = AnimationController( + vsync: this, + duration: Duration(milliseconds: widget.animDurationMillis), + ); + for (WonkyAnimSetting s in settings) { + final curve = CurvedAnimation( + parent: _animController, + curve: Interval(s.startAt, s.endAt, curve: s.curve), + ); + late Animation animation; + if (s.property == 'color') { + animation = ColorTween( + begin: s.fromTo.fromValue() as Color?, + end: s.fromTo.toValue() as Color?, + ).animate(curve); + } else { + animation = Tween( + begin: s.fromTo.fromValue() as double, + end: s.fromTo.toValue() as double, + ).animate(curve); + } + if (s.type == 'fv') { + _fvAxes.add(s.property); + _fvAnimations.add(animation); + } else if (s.type == 'basic') { + switch (s.property) { + case 'scale': + { + _scaleAnimation = animation; + } + case 'rotation': + { + _rotationAnimation = animation; + } + case 'offsetX': + { + _offsetXAnimation = animation; + } + case 'offsetY': + { + _offsetYAnimation = animation; + } + case 'color': + { + _colorAnimation = animation; + } + default: + { + if (kDebugMode) { + print( + '**ERROR** unrecognized property to animate: ${s.property}', + ); + } + } + } + } + // save refs to all curves just to persist in mem, don't need to touch them again + _curves.add(curve); + } + } +} + +abstract class WCRange { + WCRange(); + T fromValue(); + T toValue(); +} + +class RangeColor implements WCRange { + Color from; + Color to; + RangeColor({required this.from, required this.to}); + @override + Color fromValue() { + return from; + } + + @override + Color toValue() { + return to; + } +} + +class RangeDbl implements WCRange { + double from; + double to; + + RangeDbl({required this.from, required this.to}); + + @override + double fromValue() { + return from; + } + + @override + double toValue() { + return to; + } +} + +class WonkyAnimSetting { + // just the animation + String type; // 'fv' for fontVariation, 'basic' for everything else + String property; //font variation axis, or 'size'/'rotation'/etc. + WCRange fromTo; + double startAt; // 0 to 1 rel to controller + double endAt; // same as start + Curve curve; + WonkyAnimSetting({ + required this.type, + required this.property, + required this.fromTo, + required this.startAt, + required this.endAt, + required this.curve, + }); +} diff --git a/varfont_shader_puzzle/lib/main.dart b/varfont_shader_puzzle/lib/main.dart new file mode 100644 index 0000000..bf845e2 --- /dev/null +++ b/varfont_shader_puzzle/lib/main.dart @@ -0,0 +1,24 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import '../page_content/pages_flow.dart'; + +void main() { + runApp(const TypePuzzle()); +} + +class TypePuzzle extends StatelessWidget { + const TypePuzzle({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Type Jam', + theme: ThemeData(primarySwatch: Colors.grey), + home: const Scaffold(appBar: null, body: PagesFlow()), + ); + } +} diff --git a/varfont_shader_puzzle/lib/model/puzzle_model.dart b/varfont_shader_puzzle/lib/model/puzzle_model.dart new file mode 100644 index 0000000..f1dfd6c --- /dev/null +++ b/varfont_shader_puzzle/lib/model/puzzle_model.dart @@ -0,0 +1,50 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +class PuzzleModel { + final int dim; // num tiles in any one dir; dim x dim board + + // 2d array like a board + // x is the tileID and its position in the array mirrors the board + List> positions = [[]]; + + // rotation states, where index == tileID + // x is num of CCW rotations off from correct (x % 4 == 0 indicates correct) + List status = []; + + PuzzleModel({required this.dim}) { + for (int i = 0; i < dim; i++) { + if (positions[positions.length - 1].length == dim) { + positions.add([]); + } + positions[positions.length - 1].add(i); + status.add(0); + } + } + + bool allRotationsCorrect() { + for (int i = 0; i < status.length; i++) { + if (status[i] % 4 != 0) { + return false; + } + } + return true; + } + + void setTileStatus(int tileID, int newStatus) { + status[tileID] = newStatus; + } + + int getTileStatus(int tileID) { + return status[tileID]; + } + + void rotateTile(int tileID) { + status[tileID]--; + } + + int getRotationOfTile(int tileID) { + return status[tileID]; + } +} diff --git a/varfont_shader_puzzle/lib/page_content/page_ascender_descender.dart b/varfont_shader_puzzle/lib/page_content/page_ascender_descender.dart new file mode 100644 index 0000000..ce9c13d --- /dev/null +++ b/varfont_shader_puzzle/lib/page_content/page_ascender_descender.dart @@ -0,0 +1,211 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../components/components.dart'; +import '../page_content/pages_flow.dart'; +import '../styles.dart'; + +class PageAscenderDescender extends SinglePage { + const PageAscenderDescender({super.key, required super.pageConfig}); + @override + State createState() => _PageAscenderDescenderState(); +} + +class _PageAscenderDescenderState extends SinglePageState { + @override + Widget createTopicIntro() { + return LightboxedPanel( + pageConfig: widget.pageConfig, + content: [ + Text( + 'Ascenders & Descenders'.toUpperCase(), + style: TextStyles.headlineStyle(), + textAlign: TextAlign.left, + ), + Text( + 'Fonts can also vary based on their ' + 'individual pieces, like the ascenders (the parts that ' + 'extend upward) and the descenders (which extend downward)! ' + 'Piece this letter together and lock in its ' + 'wobbly ascenders and descenders!', + style: TextStyles.bodyStyle(), + textAlign: TextAlign.left, + ), + ], + ); + } + + @override + List buildWonkyChars() { + return [ + Positioned( + left: widget.pageConfig.wonkyCharLargeSize * 0.08, + top: widget.pageConfig.wonkyCharLargeSize * -0.1, + child: WonkyChar( + text: 'l', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: 0.15 * pi, + animDurationMillis: 3200, + animationSettings: [ + WonkyAnimPalette.ascenderHt( + from: 500, + to: 983, + curve: Curves.easeInOut, + ), + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + curve: Curves.easeInOut, + ), + ], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.34, + top: widget.pageConfig.wonkyCharLargeSize * 0.12, + child: WonkyChar( + text: 'g', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.12 * pi, + animDurationMillis: 3200, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + curve: Curves.easeInOut, + ), + WonkyAnimPalette.descenderDepth(from: -500, to: -138), + ], + ), + ), + Positioned( + right: widget.pageConfig.wonkyCharLargeSize * -0.1, + top: widget.pageConfig.wonkyCharLargeSize * -0.5, + child: WonkyChar( + text: 'q', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: 0.15 * pi, + animDurationMillis: 5000, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + WonkyAnimPalette.descenderDepth( + from: -240, + to: -440, + startAt: 0.3, + endAt: 0.7, + curve: Curves.bounceOut, + ), + ], + ), + ), + // lower half -------------------------------------- + Positioned( + left: widget.pageConfig.wonkyCharSmallSize * 0.1, + bottom: widget.pageConfig.wonkyCharSmallSize * -0.34, + child: WonkyChar( + text: 'f', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.15 * pi, + animDurationMillis: 12000, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + WonkyAnimPalette.ascenderHt(from: 600, to: 980), + ], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.17, + bottom: widget.pageConfig.wonkyCharLargeSize * 0.5, + child: WonkyChar( + text: 'p', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.15 * pi, + animDurationMillis: 3000, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + WonkyAnimPalette.descenderDepth( + from: -390, + to: -220, + curve: Curves.linear, + ), + ], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.4, + bottom: widget.pageConfig.wonkyCharSmallSize * 0.25, + child: WonkyChar( + text: 'k', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.15 * pi, + animDurationMillis: 3000, + animationSettings: [ + WonkyAnimPalette.ascenderHt( + from: 600, + to: 840, + curve: Curves.linear, + ), + ], + ), + ), + Positioned( + right: widget.pageConfig.wonkyCharLargeSize * 0.05, + bottom: widget.pageConfig.wonkyCharLargeSize * -0.04, + child: WonkyChar( + text: 'j', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: 0.2 * pi, + animDurationMillis: 5000, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + WonkyAnimPalette.descenderDepth(from: -200, to: -500), + ], + ), + ), + ]; + } + + @override + Widget createPuzzle() { + return RotatorPuzzle( + pageConfig: widget.pageConfig, + numTiles: 9, + puzzleNum: 3, + shader: Shader.rowOffset, + shaderDuration: 2000, + tileShadedString: 'fyd', + tileShadedStringPadding: EdgeInsets.only( + top: 0.233 * widget.pageConfig.puzzleSize, + bottom: 0, + left: 0.465 * widget.pageConfig.puzzleSize, + right: 0.465 * widget.pageConfig.puzzleSize, + ), + tileShadedStringSize: 1.86 * widget.pageConfig.puzzleSize, + tileScaleModifier: 2.7, + tileShadedStringAnimDuration: 2000, + tileShadedStringAnimSettings: [ + WonkyAnimPalette.weight(from: 200, to: 200), + WonkyAnimPalette.width(from: 50, to: 50), + WonkyAnimPalette.ascenderHt(from: 700, to: 980), + WonkyAnimPalette.descenderDepth(from: -238, to: -138), + ], + ); + } +} diff --git a/varfont_shader_puzzle/lib/page_content/page_narrative_post.dart b/varfont_shader_puzzle/lib/page_content/page_narrative_post.dart new file mode 100644 index 0000000..71d37c4 --- /dev/null +++ b/varfont_shader_puzzle/lib/page_content/page_narrative_post.dart @@ -0,0 +1,49 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import '../components/components.dart'; +import '../page_content/pages_flow.dart'; +import '../styles.dart'; + +class PageNarrativePost extends NarrativePage { + const PageNarrativePost({super.key, required super.pageConfig}); + + @override + State createState() => _PageNarrativePostState(); +} + +class _PageNarrativePostState extends NarrativePageState { + @override + void initState() { + panels = [ + LightboxedPanel( + pageConfig: widget.pageConfig, + fadeOnDismiss: false, + buildButton: true, + onDismiss: super.handleIntroDismiss, + content: [ + Text( + 'Whew, we put everything back together just before the font launch.', + style: TextStyles.bodyStyle(), + textAlign: TextAlign.left, + ), + const SizedBox(height: 8), + const Image(image: AssetImage('assets/images/specimen-1.png')), + Text( + 'As a reward, please enjoy the FontCo wallpapers on the next screen. Congratulations!', + style: TextStyles.bodyStyle(), + textAlign: TextAlign.left, + ), + ], + ), + ]; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return panels[panelIndex]; + } +} diff --git a/varfont_shader_puzzle/lib/page_content/page_narrative_pre.dart b/varfont_shader_puzzle/lib/page_content/page_narrative_pre.dart new file mode 100644 index 0000000..4f78f07 --- /dev/null +++ b/varfont_shader_puzzle/lib/page_content/page_narrative_pre.dart @@ -0,0 +1,149 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import '../components/components.dart'; +import '../page_content/pages_flow.dart'; +import '../styles.dart'; + +class PageNarrativePre extends NarrativePage { + const PageNarrativePre({super.key, required super.pageConfig}); + + @override + State createState() => _PageNarrativePreState(); +} + +class _PageNarrativePreState extends NarrativePageState { + @override + void initState() { + panels = [ + LightboxedPanel( + key: UniqueKey(), + pageConfig: widget.pageConfig, + fadeOnDismiss: false, + onDismiss: super.handleIntroDismiss, + content: [ + Text( + 'Welcome to your first day on the FontCo team! Are you ready to help us publish our newest font, Designer Pro?', + style: TextStyles.bodyStyle(), + textAlign: TextAlign.left, + ), + const SizedBox(height: 8), + const Image(image: AssetImage('assets/images/specimen-1.png')), + ], + ), + LightboxedPanel( + key: UniqueKey(), + pageConfig: widget.pageConfig, + fadeOnDismiss: false, + onDismiss: super.handleIntroDismiss, + autoDismissAfter: 100, + buildButton: false, + lightBoxBgColor: Colors.black, + cardBgColor: Colors.black, + content: [ + Transform.scale( + scaleX: -1, + child: Text( + 'Welcome to your first day on the FontCo team! Are you ready to help us publish our newest font, Designer Pro?', + style: TextStyles.bodyStyle().copyWith(color: Colors.white), + textAlign: TextAlign.left, + ), + ), + const SizedBox(height: 8), + Transform.scale( + scaleX: -1, + child: const Image( + image: AssetImage('assets/images/specimen-1-glitch.png'), + ), + ), + const SizedBox(height: 56), + ], + ), + LightboxedPanel( + key: UniqueKey(), + pageConfig: widget.pageConfig, + fadeOnDismiss: false, + onDismiss: super.handleIntroDismiss, + autoDismissAfter: 100, + buildButton: false, + lightBoxBgColor: Colors.black, + cardBgColor: Colors.black, + content: [ + Transform.scale( + scaleX: -1, + child: Transform.translate( + offset: const Offset(20.0, 0.0), + child: Text( + 'Welcome to your first day on the FontCo team! Are you ready to help us publish our newest font, Designer Pro?', + style: TextStyles.bodyStyle().copyWith(color: Colors.white), + textAlign: TextAlign.left, + ), + ), + ), + const SizedBox(height: 8), + Transform.scale( + scaleX: -1, + child: Transform.translate( + offset: const Offset(-20.0, 0.0), + child: const Image( + image: AssetImage('assets/images/specimen-1-glitch.png'), + ), + ), + ), + const SizedBox(height: 56), + ], + ), + LightboxedPanel( + key: UniqueKey(), + pageConfig: widget.pageConfig, + fadeOnDismiss: false, + onDismiss: super.handleIntroDismiss, + autoDismissAfter: 100, + buildButton: false, + lightBoxBgColor: Colors.black, + cardBgColor: Colors.black, + content: [ + Transform.scale( + scaleX: -1, + child: Text( + 'Welcome to your first day on the FontCo team! Are you ready to help us publish our newest font, Designer Pro?', + style: TextStyles.bodyStyle().copyWith(color: Colors.white), + textAlign: TextAlign.left, + ), + ), + const SizedBox(height: 8), + Transform.scale( + scaleX: -1, + child: const Image( + image: AssetImage('assets/images/specimen-1-glitch.png'), + ), + ), + const SizedBox(height: 56), + ], + ), + LightboxedPanel( + key: UniqueKey(), + pageConfig: widget.pageConfig, + fadeOnDismiss: false, + onDismiss: super.handleIntroDismiss, + content: [ + Text( + 'Oh no, you clicked the button too hard! Now the font file is glitched. Help us put the letters back together so we can launch!', + style: TextStyles.bodyStyle(), + textAlign: TextAlign.left, + ), + const SizedBox(height: 8), + const Image(image: AssetImage('assets/images/specimen-2.png')), + ], + ), + ]; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return panels[panelIndex]; + } +} diff --git a/varfont_shader_puzzle/lib/page_content/page_optical_size.dart b/varfont_shader_puzzle/lib/page_content/page_optical_size.dart new file mode 100644 index 0000000..25cf974 --- /dev/null +++ b/varfont_shader_puzzle/lib/page_content/page_optical_size.dart @@ -0,0 +1,163 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../components/components.dart'; +import '../page_content/pages_flow.dart'; +import '../styles.dart'; + +class PageOpticalSize extends SinglePage { + const PageOpticalSize({super.key, required super.pageConfig}); + + @override + State createState() => _PageOpticalSizeState(); +} + +class _PageOpticalSizeState extends SinglePageState { + @override + Widget createTopicIntro() { + return LightboxedPanel( + pageConfig: widget.pageConfig, + content: [ + Text( + 'Optical Size'.toUpperCase(), + style: TextStyles.headlineStyle(), + textAlign: TextAlign.left, + ), + Text( + 'Optical size adjusts the type according to how large it will be shown. ' + 'Smaller type usually calls for less contrast between the thin and thick ' + 'parts the letter, while larger type calls for more contrast. ' + 'Put this glitching letter back together and lock in the optical size!', + style: TextStyles.bodyStyle(), + textAlign: TextAlign.left, + ), + ], + ); + } + + @override + List buildWonkyChars() { + return [ + Positioned( + left: widget.pageConfig.wonkyCharLargeSize * -0.13, + top: widget.pageConfig.wonkyCharLargeSize * -0.3, + child: WonkyChar( + text: 'O', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: 0.15 * pi, + animDurationMillis: 3200, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + curve: Curves.easeInOut, + ), + WonkyAnimPalette.opticalSize(from: 70, to: 144), + ], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.37, + top: widget.pageConfig.wonkyCharLargeSize * 0.37, + child: WonkyChar( + text: '@', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.12 * pi, + animDurationMillis: 3200, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + curve: Curves.easeInOut, + ), + WonkyAnimPalette.opticalSize(from: 78, to: 8), + ], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.57, + top: widget.pageConfig.wonkyCharSmallSize * -0.02, + child: WonkyChar( + text: 'r', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.15 * pi, + animationSettings: [WonkyAnimPalette.opticalSize(from: 32, to: 106)], + ), + ), + Positioned( + right: widget.pageConfig.wonkyCharLargeSize * 0.03, + top: widget.pageConfig.wonkyCharLargeSize * -0.26, + child: WonkyChar( + text: 'e', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: -0.15 * pi, + animDurationMillis: 5000, + animationSettings: [WonkyAnimPalette.opticalSize(from: 70, to: 144)], + ), + ), + // lower half -------------------------------------- + Positioned( + left: widget.pageConfig.wonkyCharLargeSize * 0.1, + bottom: widget.pageConfig.wonkyCharLargeSize * 0.05, + child: WonkyChar( + text: 'i', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: -0.04 * pi, + animationSettings: [WonkyAnimPalette.opticalSize(from: 40, to: 8)], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.37, + bottom: widget.pageConfig.wonkyCharLargeSize * -0.04, + child: WonkyChar( + text: 'Z', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.15 * pi, + animationSettings: [WonkyAnimPalette.opticalSize(from: 8, to: 60)], + ), + ), + Positioned( + right: widget.pageConfig.wonkyCharLargeSize * -0.14, + bottom: widget.pageConfig.wonkyCharLargeSize * -0.1, + child: WonkyChar( + text: 'A', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: 0.15 * pi, + animDurationMillis: 12000, + animationSettings: [ + WonkyAnimPalette.opticalSize(from: 80, to: 20), + WonkyAnimPalette.rotation(from: -0.01 * pi, to: 0.01 * pi), + ], + ), + ), + ]; + } + + @override + Widget createPuzzle() { + return RotatorPuzzle( + pageConfig: widget.pageConfig, + numTiles: 16, + puzzleNum: 4, + shader: Shader.wavy, + shaderDuration: 5000, + tileShadedString: 'Z', + tileShadedStringPadding: EdgeInsets.only( + bottom: 0.349 * widget.pageConfig.puzzleSize, + ), + tileScaleModifier: 2.6, + tileShadedStringSize: 2.79 * widget.pageConfig.puzzleSize, + tileShadedStringAnimDuration: 3000, + tileShadedStringAnimSettings: [ + WonkyAnimPalette.weight(from: 1000, to: 1000), + WonkyAnimPalette.width(from: 125, to: 125), + WonkyAnimPalette.opticalSize(from: 8, to: 144), + ], + ); + } +} diff --git a/varfont_shader_puzzle/lib/page_content/page_weight.dart b/varfont_shader_puzzle/lib/page_content/page_weight.dart new file mode 100644 index 0000000..da93a7f --- /dev/null +++ b/varfont_shader_puzzle/lib/page_content/page_weight.dart @@ -0,0 +1,160 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../components/components.dart'; +import '../page_content/pages_flow.dart'; +import '../styles.dart'; + +class PageWeight extends SinglePage { + const PageWeight({super.key, required super.pageConfig}); + + @override + State createState() => _PageWeightState(); +} + +class _PageWeightState extends SinglePageState { + @override + Widget createTopicIntro() { + return LightboxedPanel( + pageConfig: widget.pageConfig, + content: [ + Text( + 'Weight'.toUpperCase(), + style: TextStyles.headlineStyle(), + textAlign: TextAlign.left, + ), + Text( + 'You probably knew that fonts can vary by weight, or the boldness, ' + 'as we can see in the letters on this page. Tap the pieces of the ' + 'broken letter to bring it back together, but don’t get distracted ' + 'by its oscillating weight!', + style: TextStyles.bodyStyle(), + textAlign: TextAlign.left, + ), + ], + ); + } + + @override + List buildWonkyChars() { + return [ + Positioned( + left: widget.pageConfig.wonkyCharLargeSize * -0.01, + top: widget.pageConfig.wonkyCharLargeSize * -0.26, + child: WonkyChar( + text: 'S', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: 0.15 * pi, + animDurationMillis: 3200, + animationSettings: [ + WonkyAnimPalette.weight( + from: 100, + to: 300, + curve: Curves.easeInOut, + ), + ], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.34, + top: widget.pageConfig.wonkyCharLargeSize * 0.3, + child: WonkyChar( + text: 't', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.12 * pi, + animDurationMillis: 3200, + animationSettings: [ + WonkyAnimPalette.weight( + from: 1000, + to: 800, + curve: Curves.easeInOut, + ), + ], + ), + ), + Positioned( + right: widget.pageConfig.wonkyCharLargeSize * 0.07, + top: widget.pageConfig.wonkyCharLargeSize * -0.26, + child: WonkyChar( + text: 'q', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: -0.15 * pi, + animDurationMillis: 5000, + animationSettings: [WonkyAnimPalette.weight(from: 200, to: 500)], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.5, + top: widget.pageConfig.wonkyCharSmallSize * 0.3, + child: WonkyChar( + text: '*', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.15 * pi, + animationSettings: [WonkyAnimPalette.weight(from: 100, to: 400)], + ), + ), + // lower half -------------------------------------- + Positioned( + left: widget.pageConfig.wonkyCharLargeSize * -0.2, + bottom: widget.pageConfig.wonkyCharLargeSize * -0.34, + child: WonkyChar( + text: 'C', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: -0.15 * pi, + animDurationMillis: 7000, + animationSettings: [WonkyAnimPalette.weight(from: 1000, to: 700)], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.42, + bottom: widget.pageConfig.wonkyCharLargeSize * 0.02, + child: WonkyChar( + text: 'f', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.15 * pi, + animDurationMillis: 4000, + animationSettings: [WonkyAnimPalette.weight(from: 100, to: 200)], + ), + ), + Positioned( + right: widget.pageConfig.wonkyCharLargeSize * -0.2, + bottom: widget.pageConfig.wonkyCharLargeSize * -0.23, + child: WonkyChar( + text: 'R', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: -1.15 * pi, + animDurationMillis: 2000, + animationSettings: [WonkyAnimPalette.weight(from: 700, to: 900)], + ), + ), + ]; + } + + @override + Widget createPuzzle() { + return RotatorPuzzle( + pageConfig: widget.pageConfig, + numTiles: 9, + puzzleNum: 1, + shader: Shader.wavy2, + shaderDuration: 3000, + tileShadedString: 'W', + tileShadedStringPadding: EdgeInsets.only( + left: 0.698 * widget.pageConfig.puzzleSize, + right: 0.698 * widget.pageConfig.puzzleSize, + ), + tileShadedStringSize: 2.79 * widget.pageConfig.puzzleSize, + tileScaleModifier: 2.4, + tileShadedStringAnimDuration: 1000, + tileShadedStringAnimSettings: [ + WonkyAnimPalette.weight(from: 600, to: 1000), + WonkyAnimPalette.width(from: 50, to: 50), + ], + ); + } +} diff --git a/varfont_shader_puzzle/lib/page_content/page_width.dart b/varfont_shader_puzzle/lib/page_content/page_width.dart new file mode 100644 index 0000000..1e77117 --- /dev/null +++ b/varfont_shader_puzzle/lib/page_content/page_width.dart @@ -0,0 +1,190 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../components/components.dart'; +import '../page_content/pages_flow.dart'; +import '../styles.dart'; + +class PageWidth extends SinglePage { + const PageWidth({super.key, required super.pageConfig}); + @override + State createState() => _PageWidthState(); +} + +class _PageWidthState extends SinglePageState { + @override + Widget createTopicIntro() { + return LightboxedPanel( + pageConfig: widget.pageConfig, + content: [ + Text( + 'Width'.toUpperCase(), + style: TextStyles.headlineStyle(), + textAlign: TextAlign.left, + ), + Text( + 'Fonts can vary by width as well. Choosing a new width setting is better ' + 'than stretching letters in an image editor, which would just distort the letter. ' + 'Solve this letter puzzle to clear the glitch and set the width!', + style: TextStyles.bodyStyle(), + textAlign: TextAlign.left, + ), + ], + ); + } + + @override + List buildWonkyChars() { + return [ + Positioned( + left: widget.pageConfig.wonkyCharLargeSize * -0.17, + top: widget.pageConfig.wonkyCharLargeSize * -0.2, + child: WonkyChar( + text: 'r', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: -0.15 * pi, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + WonkyAnimPalette.width(from: 120, to: 125), + ], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.3, + top: widget.pageConfig.wonkyCharLargeSize * 0.42, + child: WonkyChar( + text: 'x', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.12 * pi, + animDurationMillis: 3200, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + curve: Curves.easeInOut, + ), + WonkyAnimPalette.width(from: 70, to: 50), + WonkyAnimPalette.offsetY(from: -6, to: 2, curve: Curves.easeInOut), + WonkyAnimPalette.rotation(from: -0.04 * pi, to: 0.005 * pi), + ], + ), + ), + Positioned( + right: widget.pageConfig.wonkyCharLargeSize * 0, + top: widget.pageConfig.wonkyCharLargeSize * -0.2, + child: WonkyChar( + text: 'F', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: 0.15 * pi, + animDurationMillis: 3200, + animationSettings: [ + WonkyAnimPalette.width(from: 120, to: 125), + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + ], + ), + ), + + // lower half -------------------------------------- + Positioned( + left: widget.pageConfig.wonkyCharLargeSize * -0.2, + bottom: widget.pageConfig.wonkyCharLargeSize * -0.3, + child: WonkyChar( + text: 'W', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: -0.15 * pi, + animDurationMillis: 6000, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + WonkyAnimPalette.width(from: 75, to: 50), + ], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.4, + bottom: widget.pageConfig.wonkyCharLargeSize * 0.1, + child: WonkyChar( + text: 'h', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.15 * pi, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + WonkyAnimPalette.width(from: 90, to: 115), + ], + ), + ), + Positioned( + left: widget.pageConfig.screenWidth * 0.75, + bottom: widget.pageConfig.wonkyCharSmallSize * -0.24, + child: WonkyChar( + text: 'K', + size: widget.pageConfig.wonkyCharSmallSize, + baseRotation: -0.15 * pi, + animDurationMillis: 5000, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + WonkyAnimPalette.width(from: 125, to: 90, startAt: 0.3, endAt: 0.7), + ], + ), + ), + Positioned( + right: widget.pageConfig.wonkyCharLargeSize * 0.0, + bottom: widget.pageConfig.wonkyCharLargeSize * 0.1, + child: WonkyChar( + text: '?', + size: widget.pageConfig.wonkyCharLargeSize, + baseRotation: -1.67 * pi, + animationSettings: [ + WonkyAnimPalette.weight( + from: PageConfig.baseWeight, + to: PageConfig.baseWeight, + ), + WonkyAnimPalette.width(from: 110, to: 60), + ], + ), + ), + ]; + } + + @override + Widget createPuzzle() { + return RotatorPuzzle( + pageConfig: widget.pageConfig, + numTiles: 16, + puzzleNum: 2, + shader: Shader.bwSplit, + shaderDuration: 2000, + tileShadedString: 'S', + tileShadedStringPadding: EdgeInsets.only( + left: 0.349 * widget.pageConfig.puzzleSize, + right: 0.349 * widget.pageConfig.puzzleSize, + ), + tileShadedStringSize: 3.256 * widget.pageConfig.puzzleSize, + tileScaleModifier: 2.34, + tileShadedStringAnimDuration: 2000, + tileShadedStringAnimSettings: [ + WonkyAnimPalette.weight(from: 200, to: 200), + WonkyAnimPalette.width(from: 50, to: 125), + ], + ); + } +} diff --git a/varfont_shader_puzzle/lib/page_content/pages.dart b/varfont_shader_puzzle/lib/page_content/pages.dart new file mode 100644 index 0000000..602f1ab --- /dev/null +++ b/varfont_shader_puzzle/lib/page_content/pages.dart @@ -0,0 +1,10 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'page_ascender_descender.dart'; +export 'page_narrative_post.dart'; +export 'page_narrative_pre.dart'; +export 'page_optical_size.dart'; +export 'page_weight.dart'; +export 'page_width.dart'; diff --git a/varfont_shader_puzzle/lib/page_content/pages_flow.dart b/varfont_shader_puzzle/lib/page_content/pages_flow.dart new file mode 100644 index 0000000..607d9c7 --- /dev/null +++ b/varfont_shader_puzzle/lib/page_content/pages_flow.dart @@ -0,0 +1,155 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../components/components.dart'; +import '../page_content/wallpapers_flow.dart'; +import 'pages.dart'; + +class PagesFlow extends StatefulWidget { + const PagesFlow({super.key}); + + static const pageScrollDuration = 400; + + @override + State createState() => _PagesFlowState(); +} + +class _PagesFlowState extends State { + late PageController pageController = PageController(); + + @override + Widget build(BuildContext context) { + final double screenWidth = MediaQuery.of(context).size.width; + final double screenHeight = MediaQuery.of(context).size.height; + bool narrowView = screenWidth * 1.8 < screenHeight ? true : false; + double puzzleSize = + narrowView ? screenWidth * 1 : min(screenHeight * 0.6, screenWidth); + double topBottomMargin = (screenHeight - puzzleSize) * 0.5; + double wonkyCharLargeSize = topBottomMargin * 1.0; + double wonkyCharSmallSize = wonkyCharLargeSize * 0.5; + PageConfig pageConfig = PageConfig( + screenWidth: screenWidth, + screenHeight: screenHeight, + narrowView: narrowView, + puzzleSize: puzzleSize, + pageController: pageController, + wonkyCharLargeSize: wonkyCharLargeSize, + wonkyCharSmallSize: wonkyCharSmallSize, + ); + + return PageView( + controller: pageController, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + children: [ + PageNarrativePre(pageConfig: pageConfig), + PageWeight(pageConfig: pageConfig), + PageAscenderDescender(pageConfig: pageConfig), + PageOpticalSize(pageConfig: pageConfig), + PageWidth(pageConfig: pageConfig), + PageNarrativePost(pageConfig: pageConfig), + const WallpapersFlow(), + ], + ); + } +} + +class PageConfig { + final double screenWidth; + final double screenHeight; + final bool narrowView; + final double puzzleSize; + final PageController pageController; + final double wonkyCharLargeSize; + final double wonkyCharSmallSize; + static double baseWeight = 800; + const PageConfig({ + Key? key, + required this.screenWidth, + required this.screenHeight, + required this.narrowView, + required this.puzzleSize, + required this.pageController, + required this.wonkyCharLargeSize, + required this.wonkyCharSmallSize, + }); +} + +class SinglePage extends StatefulWidget { + final PageConfig pageConfig; + const SinglePage({super.key, required this.pageConfig}); + + @override + State createState() => SinglePageState(); +} + +class SinglePageState extends State with TickerProviderStateMixin { + List buildWonkyChars() { + return []; + } + + Widget createPuzzle() { + return Container(); + } + + Widget createTopicIntro() { + return LightboxedPanel(pageConfig: widget.pageConfig, content: const []); + } + + @override + Widget build(BuildContext context) { + List c = []; + c.add(createPuzzle()); + c += buildWonkyChars(); + c.add(createTopicIntro()); + return Stack(children: c); + } + + void puzzleDone() {} +} + +class NarrativePage extends StatefulWidget { + final PageConfig pageConfig; + const NarrativePage({super.key, required this.pageConfig}); + + @override + State createState() => NarrativePageState(); +} + +class NarrativePageState extends State + with TickerProviderStateMixin { + int panelIndex = 0; + List panels = []; + + void handleIntroDismiss() { + Future.delayed(const Duration(milliseconds: 50), () { + setState(() { + if (panelIndex == panels.length - 1) { + widget.pageConfig.pageController.nextPage( + duration: const Duration( + milliseconds: PagesFlow.pageScrollDuration, + ), + curve: Curves.easeOut, + ); + } else { + panelIndex++; + } + }); + }); + } + + @override + Widget build(BuildContext context) { + switch (panelIndex) { + default: + return Container(); + } + } + + void puzzleDone() {} +} diff --git a/varfont_shader_puzzle/lib/page_content/wallpapers_flow.dart b/varfont_shader_puzzle/lib/page_content/wallpapers_flow.dart new file mode 100644 index 0000000..24120c9 --- /dev/null +++ b/varfont_shader_puzzle/lib/page_content/wallpapers_flow.dart @@ -0,0 +1,426 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class WallpapersFlow extends StatefulWidget { + const WallpapersFlow({super.key}); + + @override + State createState() => _WallpapersFlowState(); +} + +class _WallpapersFlowState extends State { + int pageNum = 0; + int numPages = 4; + + @override + void initState() { + LicenseRegistry.addLicense( + () => Stream.value( + LicenseEntryWithLineBreaks(['roboto_font'], robotoLicense), + ), + ); + LicenseRegistry.addLicense( + () => Stream.value( + LicenseEntryWithLineBreaks([ + 'amstelvar_font', + ], amstelvarLicense), + ), + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + PageView( + onPageChanged: (value) { + setState(() { + pageNum = value; + }); + }, + children: const [ + DecoratedBox( + decoration: BoxDecoration(color: Colors.black), + child: Center( + child: Image( + image: AssetImage('assets/images/wallpaper3.png'), + fit: BoxFit.contain, + ), + ), + ), + DecoratedBox( + decoration: BoxDecoration(color: Colors.black), + child: Center( + child: Image( + image: AssetImage('assets/images/wallpaper1.png'), + fit: BoxFit.contain, + ), + ), + ), + DecoratedBox( + decoration: BoxDecoration(color: Colors.black), + child: Center( + child: Image( + image: AssetImage('assets/images/wallpaper2.png'), + fit: BoxFit.contain, + ), + ), + ), + LicensePage(), + ], + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 20.0), + child: Container( + width: 100, + height: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: const Color.fromARGB(220, 0, 0, 0), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: _buildScrollDots(), + ), + ), + ), + ), + ), + ], + ); + } + + List _buildScrollDots() { + List dots = []; + for (int i = 0; i < numPages; i++) { + Color dotColor = + i == pageNum + ? const Color.fromARGB(255, 255, 255, 255) + : const Color.fromARGB(255, 105, 105, 105); + Widget d = Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: dotColor, + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.white, width: 0.5), + ), + ); + dots.add(d); + } + return dots; + } + + final String robotoLicense = ''' +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + '''; + + final String amstelvarLicense = ''' +Copyright 2016 The Amstelvar Project Authors (info@fontbureau.com) + +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 & 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. + '''; +} diff --git a/varfont_shader_puzzle/lib/styles.dart b/varfont_shader_puzzle/lib/styles.dart new file mode 100644 index 0000000..996b3b8 --- /dev/null +++ b/varfont_shader_puzzle/lib/styles.dart @@ -0,0 +1,51 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +class TextStyles { + const TextStyles({Key? key}); + + static TextStyle bodyStyle() { + return const TextStyle( + fontFamily: 'Roboto', + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w400, + height: 1.5, + ); + } + + static TextStyle headlineStyle() { + return const TextStyle( + fontFamily: 'Roboto', + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w700, + height: 1.5, + ); + } +} + +class ButtonStyles { + static ButtonStyle style() { + return ButtonStyle( + fixedSize: WidgetStateProperty.resolveWith((states) { + return const Size(100, 36); + }), + shape: WidgetStateProperty.resolveWith((states) { + return const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(18)), + ); + }), + overlayColor: null, + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return Colors.black; // Hovered bg (for desktop with mouse) + } + return Colors.grey[600]; // Default bg + }), + ); + } +} diff --git a/varfont_shader_puzzle/linux/.gitignore b/varfont_shader_puzzle/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/varfont_shader_puzzle/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/varfont_shader_puzzle/linux/CMakeLists.txt b/varfont_shader_puzzle/linux/CMakeLists.txt new file mode 100644 index 0000000..ad80430 --- /dev/null +++ b/varfont_shader_puzzle/linux/CMakeLists.txt @@ -0,0 +1,145 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "varfont_shader_puzzle") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.varfont_shader_puzzle") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/varfont_shader_puzzle/linux/flutter/CMakeLists.txt b/varfont_shader_puzzle/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/varfont_shader_puzzle/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/varfont_shader_puzzle/linux/flutter/generated_plugin_registrant.cc b/varfont_shader_puzzle/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/varfont_shader_puzzle/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/varfont_shader_puzzle/linux/flutter/generated_plugin_registrant.h b/varfont_shader_puzzle/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/varfont_shader_puzzle/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/varfont_shader_puzzle/linux/flutter/generated_plugins.cmake b/varfont_shader_puzzle/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/varfont_shader_puzzle/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/varfont_shader_puzzle/linux/main.cc b/varfont_shader_puzzle/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/varfont_shader_puzzle/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/varfont_shader_puzzle/linux/my_application.cc b/varfont_shader_puzzle/linux/my_application.cc new file mode 100644 index 0000000..af4aa59 --- /dev/null +++ b/varfont_shader_puzzle/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "varfont_shader_puzzle"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "varfont_shader_puzzle"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/varfont_shader_puzzle/linux/my_application.h b/varfont_shader_puzzle/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/varfont_shader_puzzle/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/varfont_shader_puzzle/macos/.gitignore b/varfont_shader_puzzle/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/varfont_shader_puzzle/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/varfont_shader_puzzle/macos/Flutter/Flutter-Debug.xcconfig b/varfont_shader_puzzle/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/varfont_shader_puzzle/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/varfont_shader_puzzle/macos/Flutter/Flutter-Release.xcconfig b/varfont_shader_puzzle/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/varfont_shader_puzzle/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/varfont_shader_puzzle/macos/Flutter/GeneratedPluginRegistrant.swift b/varfont_shader_puzzle/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..e777c67 --- /dev/null +++ b/varfont_shader_puzzle/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import path_provider_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) +} diff --git a/varfont_shader_puzzle/macos/Podfile b/varfont_shader_puzzle/macos/Podfile new file mode 100644 index 0000000..c795730 --- /dev/null +++ b/varfont_shader_puzzle/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_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_macos_build_settings(target) + end +end diff --git a/varfont_shader_puzzle/macos/Runner.xcodeproj/project.pbxproj b/varfont_shader_puzzle/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0a1dbc3 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,791 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 9AD9C7CBBD29DB185019EF48 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1AD57E306C7913C852FBBA4 /* Pods_RunnerTests.framework */; }; + FA96D6B9CBC6E13557F9110C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A624D26F7047752B3346B282 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 02634EB403290F109558BF3C /* 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 = ""; }; + 0E2712B9A70A6FD93CF5D7E7 /* 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 = ""; }; + 10D6E80226A2DE5C9DEF84C5 /* 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 = ""; }; + 23BC1B8C8CB2FB797DD34104 /* 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 = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* varfont_shader_puzzle.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = varfont_shader_puzzle.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 38EF51A217BB444E8F1A714B /* 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 = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9DA93DF1A5822B2C98DA5037 /* 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 = ""; }; + A1AD57E306C7913C852FBBA4 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A624D26F7047752B3346B282 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9AD9C7CBBD29DB185019EF48 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FA96D6B9CBC6E13557F9110C /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 7C86745D0D82044E969BACB8 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* varfont_shader_puzzle.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 7C86745D0D82044E969BACB8 /* Pods */ = { + isa = PBXGroup; + children = ( + 0E2712B9A70A6FD93CF5D7E7 /* Pods-Runner.debug.xcconfig */, + 9DA93DF1A5822B2C98DA5037 /* Pods-Runner.release.xcconfig */, + 38EF51A217BB444E8F1A714B /* Pods-Runner.profile.xcconfig */, + 02634EB403290F109558BF3C /* Pods-RunnerTests.debug.xcconfig */, + 10D6E80226A2DE5C9DEF84C5 /* Pods-RunnerTests.release.xcconfig */, + 23BC1B8C8CB2FB797DD34104 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A624D26F7047752B3346B282 /* Pods_Runner.framework */, + A1AD57E306C7913C852FBBA4 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 1986167CB62CEBBA52986B8C /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + E9CFF6D875A4761770870406 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 74AFE9CCCD7F41E520B7612F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* varfont_shader_puzzle.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1986167CB62CEBBA52986B8C /* [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; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 74AFE9CCCD7F41E520B7612F /* [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; + }; + E9CFF6D875A4761770870406 /* [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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 02634EB403290F109558BF3C /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/varfont_shader_puzzle.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/varfont_shader_puzzle"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 10D6E80226A2DE5C9DEF84C5 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/varfont_shader_puzzle.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/varfont_shader_puzzle"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 23BC1B8C8CB2FB797DD34104 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/varfont_shader_puzzle.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/varfont_shader_puzzle"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/varfont_shader_puzzle/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/varfont_shader_puzzle/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/varfont_shader_puzzle/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/varfont_shader_puzzle/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..0bd9e46 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/varfont_shader_puzzle/macos/Runner.xcworkspace/contents.xcworkspacedata b/varfont_shader_puzzle/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/varfont_shader_puzzle/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/varfont_shader_puzzle/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/varfont_shader_puzzle/macos/Runner/AppDelegate.swift b/varfont_shader_puzzle/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/varfont_shader_puzzle/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/varfont_shader_puzzle/macos/Runner/Base.lproj/MainMenu.xib b/varfont_shader_puzzle/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/varfont_shader_puzzle/macos/Runner/Configs/AppInfo.xcconfig b/varfont_shader_puzzle/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..13b311f --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = varfont_shader_puzzle + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.varfontShaderPuzzle + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/varfont_shader_puzzle/macos/Runner/Configs/Debug.xcconfig b/varfont_shader_puzzle/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/varfont_shader_puzzle/macos/Runner/Configs/Release.xcconfig b/varfont_shader_puzzle/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/varfont_shader_puzzle/macos/Runner/Configs/Warnings.xcconfig b/varfont_shader_puzzle/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/varfont_shader_puzzle/macos/Runner/DebugProfile.entitlements b/varfont_shader_puzzle/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/varfont_shader_puzzle/macos/Runner/Info.plist b/varfont_shader_puzzle/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/varfont_shader_puzzle/macos/Runner/MainFlutterWindow.swift b/varfont_shader_puzzle/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/varfont_shader_puzzle/macos/Runner/Release.entitlements b/varfont_shader_puzzle/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/varfont_shader_puzzle/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/varfont_shader_puzzle/macos/RunnerTests/RunnerTests.swift b/varfont_shader_puzzle/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..5418c9f --- /dev/null +++ b/varfont_shader_puzzle/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/varfont_shader_puzzle/pubspec.yaml b/varfont_shader_puzzle/pubspec.yaml new file mode 100644 index 0000000..b0f032e --- /dev/null +++ b/varfont_shader_puzzle/pubspec.yaml @@ -0,0 +1,44 @@ +name: varfont_shader_puzzle +description: A new Flutter project. +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.7.0-0 + +dependencies: + flutter: + sdk: flutter + google_fonts: ^6.0.0 + +dev_dependencies: + analysis_defaults: + path: ../../analysis_defaults + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + + assets: + - assets/images/ + + shaders: + - shaders/wavy.frag + - shaders/wavy2.frag + - shaders/wavy_circ.frag + - shaders/color_split.frag + - shaders/bw_split.frag + - shaders/row_offset.frag + - shaders/nothing.frag + + fonts: + - family: Roboto + fonts: + - asset: assets/fonts/Roboto-Regular.ttf + weight: 400 + - asset: assets/fonts/Roboto-Bold.ttf + weight: 700 + - family: Amstelvar + fonts: + - asset: assets/fonts/Amstelvar-Roman[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,wdth,wght,opsz].ttf diff --git a/varfont_shader_puzzle/shaders/bw_split.frag b/varfont_shader_puzzle/shaders/bw_split.frag new file mode 100644 index 0000000..22069f8 --- /dev/null +++ b/varfont_shader_puzzle/shaders/bw_split.frag @@ -0,0 +1,41 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#define PI 3.1415926538 + +uniform float uTime; +uniform vec2 uSize; +uniform float uDampener; + +out vec4 fragColor; + +uniform sampler2D uTexture; + +void main() +{ + float piTime = uTime * PI * 2; + vec2 texCoord = gl_FragCoord.xy / uSize.xy; + float offset = 50; + + float opacSum = 0.0; + vec4 thisCol = texture(uTexture, texCoord.xy); + + float x = texCoord.x + (offset / uSize.x * pow(sin(piTime), 4)) * uDampener; + if (x >= 0.0 && x <= 1.0) { + opacSum += 0.3 * texture(uTexture, vec2(x, texCoord.y)).a; + } + + x = texCoord.x - (offset / uSize.x * pow(sin(piTime + PI), 2)) * uDampener; + if (x >= 0.0 && x <= 1.0) { + opacSum += 0.3 * texture(uTexture, vec2(x, texCoord.y)).a; + } + + float y = texCoord.y + (offset / uSize.y * pow(sin(piTime + PI * 0.66), 4)) * uDampener; + if (y >= 0.0 && y <= 1.0) { + opacSum += 0.3 * texture(uTexture, vec2(texCoord.x, y)).a; + } + + fragColor = vec4(0.0, 0.0, 0.0, clamp(opacSum, 0.0, 1.0)); + +} diff --git a/varfont_shader_puzzle/shaders/color_split.frag b/varfont_shader_puzzle/shaders/color_split.frag new file mode 100644 index 0000000..99f474b --- /dev/null +++ b/varfont_shader_puzzle/shaders/color_split.frag @@ -0,0 +1,31 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#define PI 3.1415926538 + +uniform float uTime; +uniform vec2 uSize; +uniform float uDampener; + +out vec4 fragColor; + +uniform sampler2D uTexture; + +void main() +{ + float piTime = uTime * PI * 2; + vec2 texCoord = gl_FragCoord.xy / uSize.xy; + float offset = 15; + + vec4 thisCol = texture(uTexture, texCoord.xy); + vec4 rSrc = texture(uTexture, vec2(texCoord.x + offset / uSize.x * sin(piTime), texCoord.y)); + float r = rSrc.a; + + vec4 gSrc = texture(uTexture, vec2(texCoord.x + offset / uSize.x * sin(piTime + PI), texCoord.y)); + float g = gSrc.a; + + vec4 bSrc = texture(uTexture, vec2(texCoord.x, texCoord.y + offset / uSize.y * sin(piTime + PI * 0.66))); + float b = bSrc.a; + fragColor = vec4(r, g, b, clamp(r+g+b, 0.0, 1.0)); +} diff --git a/varfont_shader_puzzle/shaders/nothing.frag b/varfont_shader_puzzle/shaders/nothing.frag new file mode 100644 index 0000000..a9a0840 --- /dev/null +++ b/varfont_shader_puzzle/shaders/nothing.frag @@ -0,0 +1,20 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#define PI 3.1415926538 + +uniform float uTime; +uniform vec2 uSize; +uniform float uDampener; + +out vec4 fragColor; + +uniform sampler2D uTexture; + +void main() +{ + float piTime = uTime * PI * 2; + vec2 texCoord = gl_FragCoord.xy / uSize.xy; + fragColor = texture(uTexture, texCoord); +} diff --git a/varfont_shader_puzzle/shaders/row_offset.frag b/varfont_shader_puzzle/shaders/row_offset.frag new file mode 100644 index 0000000..74e4ba7 --- /dev/null +++ b/varfont_shader_puzzle/shaders/row_offset.frag @@ -0,0 +1,36 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#define PI 3.1415926538 + +uniform float uTime; +uniform vec2 uSize; +uniform float uDampener; + +out vec4 fragColor; + +uniform sampler2D uTexture; + +void main() +{ + float piTime = uTime * PI * 2; + + vec2 texCoord = gl_FragCoord.xy / uSize.xy; + + float levels = 5; + float maxMag = 0.1; + float minMag = 0.02; + float magMod = maxMag / levels; + float row = floor(texCoord.y * uSize.y * 0.25); // resolution/density of rows + float offsetDir = mod(row, 2) == 0 ? -1 : 1; + float sinFn = cos(texCoord.y * 1 * PI + piTime); + float offset = (offsetDir * (minMag + maxMag * sinFn)) * uDampener; + + vec2 offsetTexCoord = vec2(texCoord.x + offset, texCoord.y); + vec4 outColor = texture(uTexture, offsetTexCoord); + if (texCoord.x + offset < 0.0 || texCoord.x + offset > 1.0) { + outColor = vec4(0.0, 0.0, 0.0, 0.0); + } + fragColor = outColor; +} diff --git a/varfont_shader_puzzle/shaders/wavy.frag b/varfont_shader_puzzle/shaders/wavy.frag new file mode 100644 index 0000000..89d5dda --- /dev/null +++ b/varfont_shader_puzzle/shaders/wavy.frag @@ -0,0 +1,31 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#define PI 3.1415926538 + +uniform float uTime; +uniform vec2 uSize; +uniform float uDampener; + +out vec4 fragColor; + +uniform sampler2D uTexture; + +void main() +{ + float piTime = uTime * PI * 2; + + vec2 texCoord = gl_FragCoord.xy / uSize.xy; + int speed; + + // wavy + speed = 1; + float xAdj = texCoord.x * 3 * PI; + float waveFnVal = sin((xAdj + piTime * speed)); + float hackAdj = 0.0; + float offset = ( ((pow(waveFnVal, 2) * 0.5 - 0.5) * 0.2) + hackAdj ) * uDampener; + + vec2 offsetTexCoord = vec2(texCoord.x, texCoord.y + offset); + fragColor = texture(uTexture, offsetTexCoord); +} diff --git a/varfont_shader_puzzle/shaders/wavy2.frag b/varfont_shader_puzzle/shaders/wavy2.frag new file mode 100644 index 0000000..da92fe1 --- /dev/null +++ b/varfont_shader_puzzle/shaders/wavy2.frag @@ -0,0 +1,27 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#define PI 3.1415926538 + +uniform float uTime; +uniform vec2 uSize; +uniform float uDampener; + +out vec4 fragColor; + +uniform sampler2D uTexture; + +void main() +{ + float piTime = uTime * PI * 2; + + vec2 texCoord = gl_FragCoord.xy / uSize.xy; + float maxMag = 0.2; + + float thisMag = (sin(texCoord.y * 10 + piTime) + 1) * 0.5 * maxMag * uDampener; + float srcX; + srcX = texCoord.x + (0.5 - texCoord.x) * thisMag; + vec2 srcCoord = vec2(srcX, texCoord.y); + fragColor = texture(uTexture, srcCoord); +} diff --git a/varfont_shader_puzzle/shaders/wavy_circ.frag b/varfont_shader_puzzle/shaders/wavy_circ.frag new file mode 100644 index 0000000..9d0c2ce --- /dev/null +++ b/varfont_shader_puzzle/shaders/wavy_circ.frag @@ -0,0 +1,39 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#define PI 3.1415926538 + +uniform float uTime; +uniform vec2 uSize; +uniform float uDampener; + +out vec4 fragColor; + +uniform sampler2D uTexture; + +void main() +{ + float piTime = uTime * PI * 2; + + vec2 texCoord = gl_FragCoord.xy / uSize.xy; + float maxMag = 0.4; + float minMag = 0.3; + float numRings = 6.0; + float ringVel = 4.0; + float numPeakShifts = 8.0; + float peakShiftVel = -3.0; + + float unitX = (texCoord.x - 0.5) * 2; + float unitY = (texCoord.y - 0.5) * 2; + float dist = distance(vec2(0, 0), vec2(unitX, unitY)); + float theta = atan(unitY, unitX) + PI; // add PI for atan2 values -PI to PI + float thisMag = (sin(theta * numRings - piTime * ringVel) + 1) * 0.5 * (cos(theta * numPeakShifts - piTime * peakShiftVel) + 1) * 0.5 * (maxMag - minMag) + minMag; + + float unitSrcDist = dist - dist * thisMag; + float unitSrcX = cos(theta) * unitSrcDist; + float unitSrcY = sin(theta) * unitSrcDist; + float texSrcX = unitSrcX * 0.5 + 0.5; + float texSrcY = unitSrcY * 0.5 + 0.5; + fragColor = texture(uTexture, vec2(texSrcX, texSrcY)); +} diff --git a/varfont_shader_puzzle/test/widget_test.dart b/varfont_shader_puzzle/test/widget_test.dart new file mode 100644 index 0000000..96e10aa --- /dev/null +++ b/varfont_shader_puzzle/test/widget_test.dart @@ -0,0 +1,26 @@ +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:varfont_shader_puzzle/main.dart'; + +void main() { + const welcomeText = + 'Welcome to your first day on the FontCo team! Are you ready to help us publish our newest font, Designer Pro?'; + const welcomeTextStep2 = + 'Oh no, you clicked the button too hard! Now the font file is glitched. Help us put the letters back together so we can launch!'; + + testWidgets('Initial display', (tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const TypePuzzle()); + + // Verify intro text + expect(find.text(welcomeText), findsOneWidget); + expect(find.text(welcomeTextStep2), findsNothing); + + // Verify OK button + expect(find.text('OK'), findsOneWidget); + }); +} diff --git a/varfont_shader_puzzle/windows/.gitignore b/varfont_shader_puzzle/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/varfont_shader_puzzle/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/varfont_shader_puzzle/windows/CMakeLists.txt b/varfont_shader_puzzle/windows/CMakeLists.txt new file mode 100644 index 0000000..49844b1 --- /dev/null +++ b/varfont_shader_puzzle/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(varfont_shader_puzzle LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "varfont_shader_puzzle") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/varfont_shader_puzzle/windows/flutter/CMakeLists.txt b/varfont_shader_puzzle/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/varfont_shader_puzzle/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/varfont_shader_puzzle/windows/flutter/generated_plugin_registrant.cc b/varfont_shader_puzzle/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/varfont_shader_puzzle/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/varfont_shader_puzzle/windows/flutter/generated_plugin_registrant.h b/varfont_shader_puzzle/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/varfont_shader_puzzle/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/varfont_shader_puzzle/windows/flutter/generated_plugins.cmake b/varfont_shader_puzzle/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/varfont_shader_puzzle/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/varfont_shader_puzzle/windows/runner/CMakeLists.txt b/varfont_shader_puzzle/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/varfont_shader_puzzle/windows/runner/Runner.rc b/varfont_shader_puzzle/windows/runner/Runner.rc new file mode 100644 index 0000000..d8acd83 --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "varfont_shader_puzzle" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "varfont_shader_puzzle" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "varfont_shader_puzzle.exe" "\0" + VALUE "ProductName", "varfont_shader_puzzle" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/varfont_shader_puzzle/windows/runner/flutter_window.cpp b/varfont_shader_puzzle/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/varfont_shader_puzzle/windows/runner/flutter_window.h b/varfont_shader_puzzle/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/varfont_shader_puzzle/windows/runner/main.cpp b/varfont_shader_puzzle/windows/runner/main.cpp new file mode 100644 index 0000000..b709e9f --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"varfont_shader_puzzle", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/varfont_shader_puzzle/windows/runner/resource.h b/varfont_shader_puzzle/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/varfont_shader_puzzle/windows/runner/resources/app_icon.ico b/varfont_shader_puzzle/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/varfont_shader_puzzle/windows/runner/resources/app_icon.ico differ diff --git a/varfont_shader_puzzle/windows/runner/runner.exe.manifest b/varfont_shader_puzzle/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/varfont_shader_puzzle/windows/runner/utils.cpp b/varfont_shader_puzzle/windows/runner/utils.cpp new file mode 100644 index 0000000..b2b0873 --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/varfont_shader_puzzle/windows/runner/utils.h b/varfont_shader_puzzle/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/varfont_shader_puzzle/windows/runner/win32_window.cpp b/varfont_shader_puzzle/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/varfont_shader_puzzle/windows/runner/win32_window.h b/varfont_shader_puzzle/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/varfont_shader_puzzle/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/veggieseasons/.gitignore b/veggieseasons/.gitignore new file mode 100644 index 0000000..0542173 --- /dev/null +++ b/veggieseasons/.gitignore @@ -0,0 +1,45 @@ +# This app is designed to show how to use Flutter's cupertino package to build UI for iOS. It's not intended to run on Android, desktop, or web. +android + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/veggieseasons/.metadata b/veggieseasons/.metadata new file mode 100644 index 0000000..9c1959e --- /dev/null +++ b/veggieseasons/.metadata @@ -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: "5874a72aa4c779a02553007c47dacbefba2374dc" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + - platform: ios + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + - platform: macos + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + + # 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' diff --git a/veggieseasons/README.md b/veggieseasons/README.md new file mode 100644 index 0000000..bf21b07 --- /dev/null +++ b/veggieseasons/README.md @@ -0,0 +1,33 @@ +# Veggie Seasons + +An iOS sample app that shows which fruits and vegetables are currently in season. It +showcases Flutter's Cupertino package. + +**NOTE:** While Flutter supports many platforms, this application is designed +specifically for iOS. It's not intended to be run on Android, web, or desktop. + +## Goals + +* Show how to build an interface that iOS users will feel right at home + with. +* Show how Flutter's Cupertino widgets work together. + +## The important bits + +### `/screens/*` + +These are the screens presented in the app, roughly analogous to +UIViewControllers. `HomeScreen` is the root, and the others are shown +as the user navigates. + +## Questions/issues + +If you have a general question about any of the techniques you see in +the sample, the best places to go are: + +* [The FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev) +* [The Flutter Gitter channel](https://gitter.im/flutter/flutter) +* [StackOverflow](https://stackoverflow.com/questions/tagged/flutter) + +If you run into an issue with the sample itself, please file an issue +in the [main Flutter repo](https://github.com/flutter/flutter/issues). diff --git a/veggieseasons/analysis_options.yaml b/veggieseasons/analysis_options.yaml new file mode 100644 index 0000000..e99e9ce --- /dev/null +++ b/veggieseasons/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:analysis_defaults/flutter.yaml + +linter: + rules: + - prefer_relative_imports \ No newline at end of file diff --git a/veggieseasons/assets/fonts/NotoSans-Bold.ttf b/veggieseasons/assets/fonts/NotoSans-Bold.ttf new file mode 100755 index 0000000..6e00cdc Binary files /dev/null and b/veggieseasons/assets/fonts/NotoSans-Bold.ttf differ diff --git a/veggieseasons/assets/fonts/NotoSans-BoldItalic.ttf b/veggieseasons/assets/fonts/NotoSans-BoldItalic.ttf new file mode 100755 index 0000000..51b7b29 Binary files /dev/null and b/veggieseasons/assets/fonts/NotoSans-BoldItalic.ttf differ diff --git a/veggieseasons/assets/fonts/NotoSans-Italic.ttf b/veggieseasons/assets/fonts/NotoSans-Italic.ttf new file mode 100755 index 0000000..dc93fea Binary files /dev/null and b/veggieseasons/assets/fonts/NotoSans-Italic.ttf differ diff --git a/veggieseasons/assets/fonts/NotoSans-Regular.ttf b/veggieseasons/assets/fonts/NotoSans-Regular.ttf new file mode 100755 index 0000000..9dd1019 Binary files /dev/null and b/veggieseasons/assets/fonts/NotoSans-Regular.ttf differ diff --git a/veggieseasons/assets/icon/launcher_icon.png b/veggieseasons/assets/icon/launcher_icon.png new file mode 100644 index 0000000..73c1b01 Binary files /dev/null and b/veggieseasons/assets/icon/launcher_icon.png differ diff --git a/veggieseasons/assets/images/apple.jpg b/veggieseasons/assets/images/apple.jpg new file mode 100644 index 0000000..7305880 Binary files /dev/null and b/veggieseasons/assets/images/apple.jpg differ diff --git a/veggieseasons/assets/images/artichoke.jpg b/veggieseasons/assets/images/artichoke.jpg new file mode 100644 index 0000000..43337dd Binary files /dev/null and b/veggieseasons/assets/images/artichoke.jpg differ diff --git a/veggieseasons/assets/images/asparagus.jpg b/veggieseasons/assets/images/asparagus.jpg new file mode 100644 index 0000000..76d340e Binary files /dev/null and b/veggieseasons/assets/images/asparagus.jpg differ diff --git a/veggieseasons/assets/images/avocado.jpg b/veggieseasons/assets/images/avocado.jpg new file mode 100644 index 0000000..16ec1e7 Binary files /dev/null and b/veggieseasons/assets/images/avocado.jpg differ diff --git a/veggieseasons/assets/images/blackberry.jpg b/veggieseasons/assets/images/blackberry.jpg new file mode 100644 index 0000000..4d8193a Binary files /dev/null and b/veggieseasons/assets/images/blackberry.jpg differ diff --git a/veggieseasons/assets/images/cantaloupe.jpg b/veggieseasons/assets/images/cantaloupe.jpg new file mode 100644 index 0000000..f048570 Binary files /dev/null and b/veggieseasons/assets/images/cantaloupe.jpg differ diff --git a/veggieseasons/assets/images/cauliflower.jpg b/veggieseasons/assets/images/cauliflower.jpg new file mode 100644 index 0000000..ce2aa91 Binary files /dev/null and b/veggieseasons/assets/images/cauliflower.jpg differ diff --git a/veggieseasons/assets/images/endive.jpg b/veggieseasons/assets/images/endive.jpg new file mode 100644 index 0000000..80c5b7e Binary files /dev/null and b/veggieseasons/assets/images/endive.jpg differ diff --git a/veggieseasons/assets/images/fig.jpg b/veggieseasons/assets/images/fig.jpg new file mode 100644 index 0000000..baaeafe Binary files /dev/null and b/veggieseasons/assets/images/fig.jpg differ diff --git a/veggieseasons/assets/images/grape.jpg b/veggieseasons/assets/images/grape.jpg new file mode 100644 index 0000000..94cd645 Binary files /dev/null and b/veggieseasons/assets/images/grape.jpg differ diff --git a/veggieseasons/assets/images/green_bell_pepper.jpg b/veggieseasons/assets/images/green_bell_pepper.jpg new file mode 100644 index 0000000..f9fac8c Binary files /dev/null and b/veggieseasons/assets/images/green_bell_pepper.jpg differ diff --git a/veggieseasons/assets/images/habanero.jpg b/veggieseasons/assets/images/habanero.jpg new file mode 100644 index 0000000..3d85abd Binary files /dev/null and b/veggieseasons/assets/images/habanero.jpg differ diff --git a/veggieseasons/assets/images/kale.jpg b/veggieseasons/assets/images/kale.jpg new file mode 100644 index 0000000..7a09929 Binary files /dev/null and b/veggieseasons/assets/images/kale.jpg differ diff --git a/veggieseasons/assets/images/kiwi.jpg b/veggieseasons/assets/images/kiwi.jpg new file mode 100644 index 0000000..21be2a6 Binary files /dev/null and b/veggieseasons/assets/images/kiwi.jpg differ diff --git a/veggieseasons/assets/images/lemon.jpg b/veggieseasons/assets/images/lemon.jpg new file mode 100644 index 0000000..62bd785 Binary files /dev/null and b/veggieseasons/assets/images/lemon.jpg differ diff --git a/veggieseasons/assets/images/lime.jpg b/veggieseasons/assets/images/lime.jpg new file mode 100644 index 0000000..a9878a5 Binary files /dev/null and b/veggieseasons/assets/images/lime.jpg differ diff --git a/veggieseasons/assets/images/mango.jpg b/veggieseasons/assets/images/mango.jpg new file mode 100644 index 0000000..27a82e5 Binary files /dev/null and b/veggieseasons/assets/images/mango.jpg differ diff --git a/veggieseasons/assets/images/mushroom.jpg b/veggieseasons/assets/images/mushroom.jpg new file mode 100644 index 0000000..a12fc69 Binary files /dev/null and b/veggieseasons/assets/images/mushroom.jpg differ diff --git a/veggieseasons/assets/images/nectarine.jpg b/veggieseasons/assets/images/nectarine.jpg new file mode 100644 index 0000000..24b55b7 Binary files /dev/null and b/veggieseasons/assets/images/nectarine.jpg differ diff --git a/veggieseasons/assets/images/orange_bell_pepper.jpg b/veggieseasons/assets/images/orange_bell_pepper.jpg new file mode 100644 index 0000000..2f78910 Binary files /dev/null and b/veggieseasons/assets/images/orange_bell_pepper.jpg differ diff --git a/veggieseasons/assets/images/persimmon.jpg b/veggieseasons/assets/images/persimmon.jpg new file mode 100644 index 0000000..c0e5d07 Binary files /dev/null and b/veggieseasons/assets/images/persimmon.jpg differ diff --git a/veggieseasons/assets/images/plum.jpg b/veggieseasons/assets/images/plum.jpg new file mode 100644 index 0000000..743c7a4 Binary files /dev/null and b/veggieseasons/assets/images/plum.jpg differ diff --git a/veggieseasons/assets/images/potato.jpg b/veggieseasons/assets/images/potato.jpg new file mode 100644 index 0000000..0f09e21 Binary files /dev/null and b/veggieseasons/assets/images/potato.jpg differ diff --git a/veggieseasons/assets/images/radicchio.jpg b/veggieseasons/assets/images/radicchio.jpg new file mode 100644 index 0000000..acd8f66 Binary files /dev/null and b/veggieseasons/assets/images/radicchio.jpg differ diff --git a/veggieseasons/assets/images/radish.jpg b/veggieseasons/assets/images/radish.jpg new file mode 100644 index 0000000..0dcbfb7 Binary files /dev/null and b/veggieseasons/assets/images/radish.jpg differ diff --git a/veggieseasons/assets/images/squash.jpg b/veggieseasons/assets/images/squash.jpg new file mode 100644 index 0000000..d20b745 Binary files /dev/null and b/veggieseasons/assets/images/squash.jpg differ diff --git a/veggieseasons/assets/images/strawberry.jpg b/veggieseasons/assets/images/strawberry.jpg new file mode 100644 index 0000000..feecd8a Binary files /dev/null and b/veggieseasons/assets/images/strawberry.jpg differ diff --git a/veggieseasons/assets/images/tangelo.jpg b/veggieseasons/assets/images/tangelo.jpg new file mode 100644 index 0000000..83c0a03 Binary files /dev/null and b/veggieseasons/assets/images/tangelo.jpg differ diff --git a/veggieseasons/assets/images/tomato.jpg b/veggieseasons/assets/images/tomato.jpg new file mode 100644 index 0000000..ab36947 Binary files /dev/null and b/veggieseasons/assets/images/tomato.jpg differ diff --git a/veggieseasons/assets/images/watermelon.jpg b/veggieseasons/assets/images/watermelon.jpg new file mode 100644 index 0000000..43880ba Binary files /dev/null and b/veggieseasons/assets/images/watermelon.jpg differ diff --git a/veggieseasons/ios/.gitignore b/veggieseasons/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/veggieseasons/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +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 diff --git a/veggieseasons/ios/Flutter/AppFrameworkInfo.plist b/veggieseasons/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/veggieseasons/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/veggieseasons/ios/Flutter/Debug.xcconfig b/veggieseasons/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/veggieseasons/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/veggieseasons/ios/Flutter/Release.xcconfig b/veggieseasons/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/veggieseasons/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/veggieseasons/ios/Podfile b/veggieseasons/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/veggieseasons/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.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! + use_modular_headers! + + 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 diff --git a/veggieseasons/ios/Runner.xcodeproj/project.pbxproj b/veggieseasons/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ab82aea --- /dev/null +++ b/veggieseasons/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*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 */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 63DF5AF6EA7E5752FD193C17 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38D833D84C3CBFFBC2A61387 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + BDDC9E5BD53006E983AC444C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C7D1011F5450B20EBA2923B /* Pods_RunnerTests.framework */; }; +/* 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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1D00FE80EC6A4B68C8E227F8 /* 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 = ""; }; + 2E0D48114CA3C6D9CCFFEF16 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 38D833D84C3CBFFBC2A61387 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5C5AC2EFB5606C655AB6C34A /* 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 = ""; }; + 5C7D1011F5450B20EBA2923B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 67C3CA414545302E36760BB5 /* 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 971FE7CB47EC26D3DD304A6B /* 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 = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FA6AACB3CDFE5B8D169FDAD3 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 63DF5AF6EA7E5752FD193C17 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C3870E06F7C4E0C4CA0F4C1C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BDDC9E5BD53006E983AC444C /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1AC2876AB73DE877F0C0B83B /* Pods */ = { + isa = PBXGroup; + children = ( + 971FE7CB47EC26D3DD304A6B /* Pods-Runner.debug.xcconfig */, + 67C3CA414545302E36760BB5 /* Pods-Runner.release.xcconfig */, + 1D00FE80EC6A4B68C8E227F8 /* Pods-Runner.profile.xcconfig */, + 2E0D48114CA3C6D9CCFFEF16 /* Pods-RunnerTests.debug.xcconfig */, + FA6AACB3CDFE5B8D169FDAD3 /* Pods-RunnerTests.release.xcconfig */, + 5C5AC2EFB5606C655AB6C34A /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 4968362A1680EFDF9222A5B5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 38D833D84C3CBFFBC2A61387 /* Pods_Runner.framework */, + 5C7D1011F5450B20EBA2923B /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 1AC2876AB73DE877F0C0B83B /* Pods */, + 4968362A1680EFDF9222A5B5 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + EEF989F59E37C6946EC40F1B /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + C3870E06F7C4E0C4CA0F4C1C /* 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 = ( + 25194613EC4035A63337D2AA /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 1F3480CB2DDBB3AE2233D2E2 /* [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 = ( + ); + 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 */ + 1F3480CB2DDBB3AE2233D2E2 /* [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; + }; + 25194613EC4035A63337D2AA /* [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; + }; + 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"; + }; + EEF989F59E37C6946EC40F1B /* [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; + }; +/* 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 */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + 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 = TC87DMJLQP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2E0D48114CA3C6D9CCFFEF16 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FA6AACB3CDFE5B8D169FDAD3 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5C5AC2EFB5606C655AB6C34A /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + 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; + "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 = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + 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 = TC87DMJLQP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 = TC87DMJLQP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */; +} diff --git a/veggieseasons/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/veggieseasons/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/veggieseasons/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/veggieseasons/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/veggieseasons/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/veggieseasons/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/veggieseasons/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/veggieseasons/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/veggieseasons/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/veggieseasons/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/veggieseasons/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..8e3ca5d --- /dev/null +++ b/veggieseasons/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/veggieseasons/ios/Runner.xcworkspace/contents.xcworkspacedata b/veggieseasons/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/veggieseasons/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/veggieseasons/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/veggieseasons/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/veggieseasons/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/veggieseasons/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/veggieseasons/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/veggieseasons/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/veggieseasons/ios/Runner/AppDelegate.swift b/veggieseasons/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/veggieseasons/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..55d5fc9 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..e3b431b Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..1861206 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..e846575 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..b12478c Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..bc7d6b4 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..a21c2b5 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..1861206 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..e494c44 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..81c4020 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..81c4020 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..27b09c6 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..15cc145 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..0374611 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..beef9b9 Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/veggieseasons/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/veggieseasons/ios/Runner/Base.lproj/LaunchScreen.storyboard b/veggieseasons/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/veggieseasons/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/veggieseasons/ios/Runner/Base.lproj/Main.storyboard b/veggieseasons/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/veggieseasons/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/veggieseasons/ios/Runner/Info.plist b/veggieseasons/ios/Runner/Info.plist new file mode 100644 index 0000000..c985223 --- /dev/null +++ b/veggieseasons/ios/Runner/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Veggie Seasons + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + veggieseasons + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/veggieseasons/ios/Runner/Runner-Bridging-Header.h b/veggieseasons/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/veggieseasons/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/veggieseasons/ios/RunnerTests/RunnerTests.swift b/veggieseasons/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/veggieseasons/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/veggieseasons/lib/data/app_state.dart b/veggieseasons/lib/data/app_state.dart new file mode 100644 index 0000000..4c4edd2 --- /dev/null +++ b/veggieseasons/lib/data/app_state.dart @@ -0,0 +1,86 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'local_veggie_provider.dart'; +import 'veggie.dart'; + +class AppState extends ChangeNotifier { + final List _veggies; + + AppState() : _veggies = LocalVeggieProvider.veggies; + + List get allVeggies => List.from(_veggies); + + List get availableVeggies { + var currentSeason = _getSeasonForDate(DateTime.now()); + return _veggies + .where((v) => v.seasons.contains(currentSeason)) + .toList(); + } + + List get favoriteVeggies => + _veggies.where((v) => v.isFavorite).toList(); + + List get unavailableVeggies { + var currentSeason = _getSeasonForDate(DateTime.now()); + return _veggies + .where((v) => !v.seasons.contains(currentSeason)) + .toList(); + } + + Veggie getVeggie(int? id) => _veggies.singleWhere((v) => v.id == id); + + List searchVeggies(String? terms) => _veggies + .where((v) => v.name.toLowerCase().contains(terms!.toLowerCase())) + .toList(); + + void setFavorite(int? id, bool isFavorite) { + var veggie = getVeggie(id); + veggie.isFavorite = isFavorite; + notifyListeners(); + } + + /// Used in tests to set the season independent of the current date. + static Season? debugCurrentSeason; + + static Season? _getSeasonForDate(DateTime date) { + if (debugCurrentSeason != null) { + return debugCurrentSeason; + } + + // Technically the start and end dates of seasons can vary by a day or so, + // but this is close enough for produce. + switch (date.month) { + case 1: + return Season.winter; + case 2: + return Season.winter; + case 3: + return date.day < 21 ? Season.winter : Season.spring; + case 4: + return Season.spring; + case 5: + return Season.spring; + case 6: + return date.day < 21 ? Season.spring : Season.summer; + case 7: + return Season.summer; + case 8: + return Season.summer; + case 9: + return date.day < 22 ? Season.autumn : Season.winter; + case 10: + return Season.autumn; + case 11: + return Season.autumn; + case 12: + return date.day < 22 ? Season.autumn : Season.winter; + default: + throw ArgumentError( + 'Can\'t return a season for month #${date.month}.', + ); + } + } +} diff --git a/veggieseasons/lib/data/local_veggie_provider.dart b/veggieseasons/lib/data/local_veggie_provider.dart new file mode 100644 index 0000000..9b387bc --- /dev/null +++ b/veggieseasons/lib/data/local_veggie_provider.dart @@ -0,0 +1,1071 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'veggie.dart'; + +class LocalVeggieProvider { + static List veggies = [ + Veggie( + id: 1, + name: 'Apples', + imageAssetPath: 'assets/images/apple.jpg', + category: VeggieCategory.fruit, + shortDescription: + 'Green or red, they\'re generally round and tasty.', + accentColor: const Color(0x40de8c66), + seasons: [ + Season.winter, + Season.spring, + Season.summer, + Season.autumn, + ], + vitaminAPercentage: 2, + vitaminCPercentage: 8, + servingSize: 'One large apple', + caloriesPerServing: 130, + trivia: const [ + Trivia( + 'A peck of apples (that\'s a real unit of mesaurement!) weighs approximately how many pounds?', + ['10 pounds', '20 pounds', '30 pounds'], + 0, + ), + Trivia('Which of these is an actual variety of apples?', [ + 'Dancing Turkey', + 'Winter Banana', + 'Red Sloth', + ], 1), + Trivia( + 'In Greek mythology, Paris gives a golden apple marked "To the Fairest" to a goddess. Which one?', + ['Hera', 'Athena', 'Aphrodite'], + 2, + ), + Trivia('Apples in the supermarket can be up to how old?', [ + '1 week', + '1 month', + '1 year', + ], 2), + Trivia( + 'How long does it take a typical apple tree to produce its first fruit?', + ['One to two years', 'Four or five years', 'Eight to ten years'], + 1, + ), + Trivia( + 'Archaeological evidence of humans eating apples dates back how far?', + ['500 C.E.', '2000 B.C.E.', '6500 B.C.E.'], + 2, + ), + Trivia('What are the seed pockets inside an apple called?', [ + 'Tarsals', + 'Carpels', + 'Sacs', + ], 1), + ], + ), + Veggie( + id: 2, + name: 'Artichokes', + imageAssetPath: 'assets/images/artichoke.jpg', + category: VeggieCategory.flower, + shortDescription: 'The armadillo of vegetables.', + accentColor: const Color(0x408ea26d), + seasons: [Season.spring, Season.autumn], + vitaminAPercentage: 0, + vitaminCPercentage: 25, + servingSize: '1 medium artichoke', + caloriesPerServing: 60, + trivia: const [ + Trivia('Artichokes are which part of the plant?', [ + 'Flower bud', + 'Root', + 'Seed', + ], 0), + Trivia( + '"Jerusalem artichoke" is another term for which vegetable?', + [ + 'Potato', + 'Cabbage', + 'Sunchoke', + ], + 2, + ), + Trivia( + 'Which city claims to be The Artichoke Capital of the World?', + [ + 'Castroville, California', + 'Galveston, Texas', + 'London, England', + ], + 0, + ), + Trivia('Artichokes are technically which type of plant?', [ + 'Thistle', + 'Azalea', + 'Tulip', + ], 0), + ], + ), + Veggie( + id: 3, + name: 'Asparagus', + imageAssetPath: 'assets/images/asparagus.jpg', + category: VeggieCategory.fern, + shortDescription: + 'It\'s been used a food and medicine for millenia.', + accentColor: const Color(0x408cb437), + seasons: [Season.spring], + vitaminAPercentage: 10, + vitaminCPercentage: 15, + servingSize: '5 spears', + caloriesPerServing: 20, + trivia: const [ + Trivia( + 'The nodules at the tip of an asparagus spear are actually which part of the plant?', + ['Seeds', 'Leaves', 'Potective scales'], + 1, + ), + Trivia('How is white asparagus made?', [ + 'It\'s watered with milk', + 'It\'s a different species', + 'It\'s grown in the shade', + ], 2), + Trivia( + 'Asapragus spears have been observed growing how many inches in a single day?', + ['1', '3', '6'], + 2, + ), + Trivia('To which flower is asparagus related?', [ + 'Lily', + 'Rose', + 'Whole wheat', + ], 0), + ], + ), + Veggie( + id: 4, + name: 'Avocado', + imageAssetPath: 'assets/images/avocado.jpg', + category: VeggieCategory.stealthFruit, + shortDescription: + 'One of the oiliest, richest fruits money can buy.', + accentColor: const Color(0x40b0ba59), + seasons: [Season.winter, Season.spring, Season.summer], + vitaminAPercentage: 0, + vitaminCPercentage: 4, + servingSize: '1/5 medium avocado', + caloriesPerServing: 50, + trivia: const [ + Trivia('What\'s the most popular variety of avocado?', [ + 'Stevenson', + 'Hass', + 'Juicy Lucy', + ], 1), + Trivia( + 'The word avocado derives from "ahuacatl," found in which civilization?', + ['Aztec', 'Incan', 'Sumerian'], + 0, + ), + Trivia('What percentage of an avocado is nutritional fat?', [ + '10', + '25', + '50', + ], 1), + Trivia( + 'The first evidence of avocado consumption by humans dates back to what year?', + ['2,000 B.C.', '6,000 B.C.', '10,000 B.C.'], + 2, + ), + ], + ), + Veggie( + id: 5, + name: 'Blackberries', + imageAssetPath: 'assets/images/blackberry.jpg', + category: VeggieCategory.berry, + shortDescription: + 'Find them on backroads and fences in the Northwest.', + accentColor: const Color(0x409d5adb), + seasons: [Season.summer], + vitaminAPercentage: 6, + vitaminCPercentage: 4, + servingSize: '1 cup', + caloriesPerServing: 62, + trivia: const [ + Trivia('What color are unripe blackberries?', [ + 'Red', + 'White', + 'Brown', + ], 0), + Trivia( + 'The blackberry is the official fruit of which American state?', + ['Washington', 'Colorado', 'Alabama'], + 2, + ), + Trivia('How many varieties of blackberries are known to exist?', [ + '500', + '1000', + '2000', + ], 2), + ], + ), + Veggie( + id: 6, + name: 'Cantaloupe', + imageAssetPath: 'assets/images/cantaloupe.jpg', + category: VeggieCategory.melon, + shortDescription: 'A fruit so tasty there\'s a utensil just for it.', + accentColor: const Color(0x40f6bd56), + seasons: [Season.summer], + vitaminAPercentage: 120, + vitaminCPercentage: 80, + servingSize: '1/4 medium cantaloupe', + caloriesPerServing: 50, + trivia: const [ + Trivia('Which of these is another name for cantaloupe?', [ + 'Muskmelon', + 'Crenshaw melon', + 'Rindfruit', + ], 0), + Trivia('The word "cantaloupe" is a reference to what?', [ + 'The Latin word for a ring of seeds', + 'The gardens of a castle in Italy', + 'An aphid species that feeds on cantaloupe leaves', + ], 1), + Trivia('Cantaloupes grow on what kind of plant?', [ + 'Tree', + 'Shrub', + 'Vine', + ], 2), + Trivia( + 'The most expensive melons in Japan can sell for up to how much?', + ['\$100', '\$1,000', '\$10,000'], + 2, + ), + ], + ), + Veggie( + id: 7, + name: 'Cauliflower', + imageAssetPath: 'assets/images/cauliflower.jpg', + category: VeggieCategory.cruciferous, + shortDescription: 'Looks like white broccoli and explodes when cut.', + accentColor: const Color(0x40c891a8), + seasons: [Season.autumn], + vitaminAPercentage: 0, + vitaminCPercentage: 100, + servingSize: '1/6 medium head', + caloriesPerServing: 25, + trivia: const [ + Trivia( + 'The quote "Cauliflower is nothing but cabbage with a college education" is attributed to whom?', + ['Cesar Romero', 'Mark Twain', 'Lucille Ball'], + 1, + ), + Trivia( + 'The edible head of a cauliflower plant is sometimes called what?', + ['The curd', 'The cow', 'The cudgel'], + 0, + ), + Trivia( + 'Cauliflower is related closest to which of these other plants?', + ['Mustard greens', 'Apples', 'Potatoes'], + 0, + ), + Trivia( + 'Cauliflower\'s green spiral-shaped cousin is known as what?', + [ + 'Romesco', + 'Brittany cabbage', + 'Muscle sprouts', + ], + 0, + ), + Trivia('Green cauliflower is sometimes called what?', [ + 'Broccoflower', + 'Avocadoflower', + 'Gross', + ], 0), + ], + ), + Veggie( + id: 8, + name: 'Endive', + imageAssetPath: 'assets/images/endive.jpg', + category: VeggieCategory.leafy, + shortDescription: 'It\'s basically the veal of lettuce.', + accentColor: const Color(0x40c5be53), + seasons: [Season.winter, Season.spring, Season.autumn], + vitaminAPercentage: 10, + vitaminCPercentage: 2, + servingSize: '1/2 cup, chopped', + caloriesPerServing: 4, + trivia: const [ + Trivia('What\'s another name for Belgian endive?', [ + 'Radicchio', + 'St. Paul\'s lettuce', + 'Witloof chicory', + ], 2), + Trivia('How does endive propagate itself?', [ + 'By seed', + 'By rhizome', + 'By packing up and moving to Des Moines', + ], 0), + Trivia( + 'Some farmers cover their endive with shade to reduce what?', + [ + 'Size', + 'Toughness', + 'Bitterness', + ], + 2, + ), + ], + ), + Veggie( + id: 9, + name: 'Figs', + imageAssetPath: 'assets/images/fig.jpg', + category: VeggieCategory.fruit, + shortDescription: 'Delicious when sliced and wrapped in prosciutto.', + accentColor: const Color(0x40aa6d7c), + seasons: [Season.summer, Season.autumn], + vitaminAPercentage: 2, + vitaminCPercentage: 2, + servingSize: '1 large fig', + caloriesPerServing: 50, + trivia: const [ + Trivia('Which of these isn\'t a variety of figs?', [ + 'Brown Turkey', + 'Green Ischia', + 'Red Racer', + ], 2), + Trivia('A fig\'s natural sugar content is around what?', [ + '25%', + '50%', + '75%', + ], 1), + Trivia('How much sun should be used to ripen figs?', [ + 'Shade', + 'Partial shade', + 'Full sun', + ], 2), + ], + ), + Veggie( + id: 10, + name: 'Grapes', + imageAssetPath: 'assets/images/grape.jpg', + category: VeggieCategory.berry, + shortDescription: 'Couldn\'t have wine without them.', + accentColor: const Color(0x40ac708a), + seasons: [Season.autumn], + vitaminAPercentage: 0, + vitaminCPercentage: 2, + servingSize: '3/4 cup', + caloriesPerServing: 90, + trivia: const [ + Trivia('How long ago were grapes introduced to the Americas?', [ + '100 years', + '200 years', + '300 years', + ], 2), + Trivia('Which of these is not an actual color of grapes?', [ + 'Pink', + 'Yellow', + 'Brown', + ], 2), + Trivia( + 'About how many millions of tons of grapes are produced each year?', + ['40', '80', '120'], + 1, + ), + Trivia('There are about how many known varieties of grapes?', [ + '2,000', + '4,000', + '8,000', + ], 2), + ], + ), + Veggie( + id: 11, + name: 'Green Pepper', + imageAssetPath: 'assets/images/green_bell_pepper.jpg', + category: VeggieCategory.stealthFruit, + shortDescription: 'Pleasantly bitter, like a sad movie.', + accentColor: const Color(0x408eb332), + seasons: [Season.summer], + vitaminAPercentage: 4, + vitaminCPercentage: 190, + servingSize: '1 medium pepper', + caloriesPerServing: 25, + trivia: const [ + Trivia('What\'s the Australian term for a bell pepper?', [ + 'Capsicum', + 'Ringer', + 'John Dobbins', + ], 0), + Trivia('How are green peppers produced?', [ + 'They\'re picked before ripening', + 'They\'re grown in the shade', + 'They\'re a distinct species', + ], 0), + Trivia( + 'How quickly can a green pepper grow from seed to harvest?', + [ + '10 weeks', + '20 weeks', + '30 weeks', + ], + 0, + ), + ], + ), + Veggie( + id: 12, + name: 'Habanero', + imageAssetPath: 'assets/images/habanero.jpg', + category: VeggieCategory.stealthFruit, + shortDescription: 'Delicious... in extremely small quantities.', + accentColor: const Color(0x40ff7a01), + seasons: [Season.summer, Season.autumn], + vitaminAPercentage: 9, + vitaminCPercentage: 100, + servingSize: '1 pepper', + caloriesPerServing: 20, + trivia: const [ + Trivia('How high can habaneros rate on the Scoville scale?', [ + '200,000 units', + '600,000 units', + '1,000,000 units', + ], 1), + Trivia( + 'Which of these is a pepper known to be hotter than the habanero?', + ['Serrano pepper', 'Hatch chile', 'Pepper X'], + 2, + ), + Trivia('Which of these isn\'t a variety of habaneros?', [ + 'White giant', + 'Condor\'s beak', + 'Saucy tyrant', + ], 2), + ], + ), + Veggie( + id: 13, + name: 'Kale', + imageAssetPath: 'assets/images/kale.jpg', + category: VeggieCategory.cruciferous, + shortDescription: + 'The meanest vegetable. Does not want to be eaten.', + accentColor: const Color(0x40a86bd8), + seasons: [Season.winter, Season.autumn], + vitaminAPercentage: 133, + vitaminCPercentage: 134, + servingSize: '1 cup, chopped', + caloriesPerServing: 33, + trivia: const [ + Trivia('Kale is sweeter when harvested after what?', [ + 'Sundown', + 'The first frost', + 'Reading it a sad story', + ], 1), + Trivia( + 'Which of these isn\'t a color in which Kale can be found?', + [ + 'Purple', + 'White', + 'Orange', + ], + 2, + ), + Trivia( + 'One serving of kale provides what percentage of a typical person\'s requirement for vitamin K?', + ['100%', '300%', '900%'], + 2, + ), + ], + ), + Veggie( + id: 14, + name: 'Kiwi', + imageAssetPath: 'assets/images/kiwi.jpg', + category: VeggieCategory.berry, + shortDescription: 'Also known as Chinese gooseberry.', + accentColor: const Color(0x40b47b37), + seasons: [Season.summer], + vitaminAPercentage: 2, + vitaminCPercentage: 240, + servingSize: '2 medium kiwis', + caloriesPerServing: 90, + trivia: const [ + Trivia('Europeans sometimes refer to kiwi as what?', [ + 'Chinese gooseberry', + 'Gem berries', + 'Bulbfruit', + ], 0), + Trivia('On what type of plant do kiwi grow?', [ + 'Tree', + 'Shrub', + 'Vine', + ], 2), + Trivia( + 'Compared to oranges, kiwi typically contain how much vitamin C?', + ['Half as much', 'About the same', 'Twice as much'], + 2, + ), + ], + ), + Veggie( + id: 15, + name: 'Lemons', + imageAssetPath: 'assets/images/lemon.jpg', + category: VeggieCategory.citrus, + shortDescription: 'Similar to limes, only yellow.', + accentColor: const Color(0x40e2a500), + seasons: [Season.winter], + vitaminAPercentage: 0, + vitaminCPercentage: 40, + servingSize: '1 medium lemon', + caloriesPerServing: 15, + trivia: const [ + Trivia( + 'A lemon tree can produce up to how many pounds of fruit each year?', + ['100', '300', '600'], + 2, + ), + Trivia('Which of these isn\'t a type of lemon?', [ + 'Acid', + 'Sarcastic', + 'Sweet', + ], 1), + Trivia('What percent of a typical lemon is composed of juice?', [ + '20%', + '40%', + '60%', + ], 0), + Trivia( + 'Which lemon variety is prized for its sweeter-than-averga flavor?', + ['Hookeye', 'Meyer', 'Minnesota Stomp'], + 1, + ), + ], + ), + Veggie( + id: 16, + name: 'Limes', + imageAssetPath: 'assets/images/lime.jpg', + category: VeggieCategory.citrus, + shortDescription: + 'Couldn\'t have ceviche and margaritas without them.', + accentColor: const Color(0x4089b733), + seasons: [Season.winter], + vitaminAPercentage: 0, + vitaminCPercentage: 35, + servingSize: '1 medium lime', + caloriesPerServing: 20, + trivia: const [ + Trivia('Which American state is famous for its Key Lime Pie?', [ + 'Pennsylvania', + 'Arizona', + 'Florida', + ], 2), + Trivia( + 'Dried limes are a particularly popular soup ingredient in which part of the world?', + ['Middle East', 'Africa', 'Australia'], + 0, + ), + Trivia( + 'Sailors once carried limes on their ships to help against which condition?', + ['Influenza', 'Scurvy', 'Boredom'], + 1, + ), + ], + ), + Veggie( + id: 17, + name: 'Mangos', + imageAssetPath: 'assets/images/mango.jpg', + category: VeggieCategory.tropical, + shortDescription: + 'A fun orange fruit popular with smoothie enthusiasts.', + accentColor: const Color(0x40fcc93c), + seasons: [Season.summer, Season.autumn], + vitaminAPercentage: 72, + vitaminCPercentage: 203, + servingSize: '1 fruit', + caloriesPerServing: 201, + trivia: const [ + Trivia( + 'In Mexico, mangos are frequently dusted with what spices before being eaten as a snack?', + [ + 'Black pepper and sugar', + 'Chile pepper and lime juice', + 'Cumin and salt', + ], + 1, + ), + Trivia('To which nut is the mango closely related?', [ + 'Cashew', + 'Peanut', + 'Walnut', + ], 0), + Trivia('In which country did mangos originate?', [ + 'India', + 'Madagascar', + 'Belize', + ], 0), + ], + ), + Veggie( + id: 18, + name: 'Mushrooms', + imageAssetPath: 'assets/images/mushroom.jpg', + category: VeggieCategory.fungus, + shortDescription: 'They\'re not truffles, but they\'re still tasty.', + accentColor: const Color(0x40ba754b), + seasons: [Season.spring, Season.autumn], + vitaminAPercentage: 0, + vitaminCPercentage: 2, + servingSize: '5 medium \'shrooms', + caloriesPerServing: 20, + trivia: const [ + Trivia('Someone who loves eating mushrooms is called what?', [ + 'A mycophagist', + 'A philologist', + 'A phlebotomist', + ], 0), + Trivia( + 'Morel mushrooms are particulary prized by cooks of which style of cuisine?', + ['French', 'Italian', 'Japanese'], + 0, + ), + Trivia( + 'The largest living organism ever identified is what type of mushroom?', + ['Shiitake mushroom', 'Honey mushroom', 'Glory mushroom'], + 1, + ), + Trivia( + 'One mushroom cousin is the truffle. Which color truffle is the most prized?', + ['White', 'Black', 'Brown'], + 0, + ), + ], + ), + Veggie( + id: 19, + name: 'Nectarines', + imageAssetPath: 'assets/images/nectarine.jpg', + category: VeggieCategory.stoneFruit, + shortDescription: 'Tiny, bald peaches.', + accentColor: const Color(0x40e45b3b), + seasons: [Season.summer], + vitaminAPercentage: 8, + vitaminCPercentage: 15, + servingSize: '1 medium nectarine', + caloriesPerServing: 60, + trivia: const [ + Trivia( + 'Nectarines are technically a variety of which other fruit?', + [ + 'Peach', + 'Plum', + 'Cherry', + ], + 0, + ), + Trivia('Nectarines are sometimes called what?', [ + 'Neckless geese', + 'Giant grapes', + 'Shaved peaches', + ], 2), + Trivia( + 'Nectarines are thought to have originated in which country?', + [ + 'China', + 'Italy', + 'Ethiopia', + ], + 0, + ), + ], + ), + Veggie( + id: 20, + name: 'Persimmons', + imageAssetPath: 'assets/images/persimmon.jpg', + category: VeggieCategory.fruit, + shortDescription: + 'It\'s like a plum and an apple had a baby together.', + accentColor: const Color(0x40979852), + seasons: [Season.winter, Season.autumn], + vitaminAPercentage: 0, + vitaminCPercentage: 27, + servingSize: '1 fruit', + caloriesPerServing: 32, + trivia: const [ + Trivia('What\'s the most commonly grown variety of persimmon?', [ + 'Sugar', + 'Yellowbird', + 'Kaki', + ], 2), + Trivia('The word "persimmon" is derived from which language?', [ + 'Latin', + 'Indo-European', + 'Powhatan', + ], 2), + Trivia('Which of these is an alternate variety of persimmon?', [ + 'Black Sapote', + 'Green Troubador', + 'Red Captain', + ], 0), + ], + ), + Veggie( + id: 21, + name: 'Plums', + imageAssetPath: 'assets/images/plum.jpg', + category: VeggieCategory.stoneFruit, + shortDescription: 'Popular in fruit salads and children\'s tales.', + accentColor: const Color(0x40e48b47), + seasons: [Season.summer], + vitaminAPercentage: 8, + vitaminCPercentage: 10, + servingSize: '2 medium plums', + caloriesPerServing: 70, + trivia: const [ + Trivia('Plums should be handled with care because...?', [ + 'They\'re particularly sticky', + 'They bruise easily', + 'It\'s easy to hurt their feelings', + ], 1), + Trivia('A dried plum is known as what?', [ + 'A prune', + 'An apricot', + 'A raisin', + ], 0), + Trivia('A sugar plum typically contains how much plum juice?', [ + '0%', + '25%', + '50%', + ], 0), + ], + ), + Veggie( + id: 22, + name: 'Potatoes', + imageAssetPath: 'assets/images/potato.jpg', + category: VeggieCategory.tuber, + shortDescription: 'King of starches and giver of french fries.', + accentColor: const Color(0x40c65c63), + seasons: [Season.winter, Season.autumn], + vitaminAPercentage: 0, + vitaminCPercentage: 45, + servingSize: '1 medium spud', + caloriesPerServing: 110, + trivia: const [ + Trivia( + 'Which country consumes the most fried potatoes per capita?', + [ + 'United States', + 'Belgium', + 'Ireland', + ], + 1, + ), + Trivia( + 'Who is credited with introducing French Fries to the United States?', + ['Thomas Jefferson', 'Betsy Ross', 'Alexander Hamilton'], + 0, + ), + Trivia( + 'The world record for loongest curly fry stands at how many inches?', + ['38', '58', '78'], + 0, + ), + ], + ), + Veggie( + id: 23, + name: 'Radicchio', + imageAssetPath: 'assets/images/radicchio.jpg', + category: VeggieCategory.leafy, + shortDescription: + 'It\'s that bitter taste in the salad you\'re eating.', + accentColor: const Color(0x40d75875), + seasons: [Season.spring, Season.autumn], + vitaminAPercentage: 0, + vitaminCPercentage: 10, + servingSize: '2 cups shredded', + caloriesPerServing: 20, + trivia: const [ + Trivia( + 'Radicchio is a particuarly good source of which mineral?', + [ + 'Manganese', + 'Mercury', + 'Molybdenum', + ], + 0, + ), + Trivia('Radicchio should be stored at what temperature?', [ + 'Room temperature', + 'Refrigerator temperature', + 'Freezer temperature', + ], 1), + Trivia( + 'What happens to the taste of radicchio once the plant flowers?', + [ + 'It becomes bitter', + 'It becomes sweeter', + 'Nothing. It just looks nicer!', + ], + 0, + ), + ], + ), + Veggie( + id: 24, + name: 'Radishes', + imageAssetPath: 'assets/images/radish.jpg', + category: VeggieCategory.root, + shortDescription: + 'Try roasting them in addition to slicing them up raw.', + accentColor: const Color(0x40819e4e), + seasons: [Season.spring, Season.autumn], + vitaminAPercentage: 0, + vitaminCPercentage: 30, + servingSize: '7 radishes', + caloriesPerServing: 10, + trivia: const [ + Trivia( + 'Which ancient civilization is known to have used radish oil?', + [ + 'Egyptian', + 'Sumerian', + 'Incan', + ], + 0, + ), + Trivia( + 'What\'s the name of the radish commonly used in Japanese cuisine?', + ['Daisuki', 'Daijin', 'Daikon'], + 2, + ), + Trivia( + 'The annual "Night of the Radishes" festival takes place just before Christmas Eve in which country?', + ['Mexico', 'France', 'South Korea'], + 0, + ), + ], + ), + Veggie( + id: 25, + name: 'Squash', + imageAssetPath: 'assets/images/squash.jpg', + category: VeggieCategory.gourd, + shortDescription: + 'Just slather them in butter and pop \'em in the oven.', + accentColor: const Color(0x40dbb721), + seasons: [Season.winter, Season.autumn], + vitaminAPercentage: 297, + vitaminCPercentage: 48, + servingSize: '1 cup diced butternut', + caloriesPerServing: 63, + trivia: const [ + Trivia('Which of these is not a type of squash?', [ + 'Zucchini', + 'Spaghetti', + 'Martini', + ], 2), + Trivia('Gourds like squash are also handy as what?', [ + 'Containers', + 'Furniture', + 'Musical instruments', + ], 0), + Trivia( + 'Which country is the world\'s largest importer of squashes?', + [ + 'China', + 'United States', + 'Russia', + ], + 1, + ), + ], + ), + Veggie( + id: 26, + name: 'Strawberries', + imageAssetPath: 'assets/images/strawberry.jpg', + category: VeggieCategory.berry, + shortDescription: + 'A delicious fruit that keeps its seeds on the outside.', + accentColor: const Color(0x40f06a44), + seasons: [Season.spring, Season.summer], + vitaminAPercentage: 0, + vitaminCPercentage: 160, + servingSize: '8 medium strawberries', + caloriesPerServing: 50, + trivia: const [ + Trivia('How many seeds are in the average strawberry?', [ + '50', + '100', + '200', + ], 2), + Trivia( + 'Strawberries are closely related to which type of flower?', + [ + 'The rose', + 'The daisy', + 'The tulip', + ], + 0, + ), + Trivia('Strawberries are unique among fruit for what reason?', [ + 'Their seeds are on the outside', + 'Their flowers are striped', + 'Their plants have a taproot', + ], 0), + ], + ), + Veggie( + id: 27, + name: 'Tangelo', + imageAssetPath: 'assets/images/tangelo.jpg', + category: VeggieCategory.citrus, + shortDescription: + 'No one\'s sure what they are or where they came from.', + accentColor: const Color(0x40f88c06), + seasons: [Season.winter, Season.autumn], + vitaminAPercentage: 6, + vitaminCPercentage: 181, + servingSize: '1 medium tangelo', + caloriesPerServing: 60, + trivia: const [ + Trivia( + 'The tangelo is thought to be a cross between oranges and which other fruit?', + ['Peach', 'Plum', 'Pummelo'], + 2, + ), + Trivia('Which of these is a variety of tangelo?', [ + 'Orlando', + 'Bluebonnet', + 'Creakey Pete', + ], 0), + Trivia('A typical tangelo is about what size?', [ + 'Golf ball', + 'Baseball', + 'Bowling ball', + ], 1), + ], + ), + Veggie( + id: 28, + name: 'Tomatoes', + imageAssetPath: 'assets/images/tomato.jpg', + category: VeggieCategory.stealthFruit, + shortDescription: 'A new world food with old world tradition.', + accentColor: const Color(0x40ea3628), + seasons: [Season.summer], + vitaminAPercentage: 20, + vitaminCPercentage: 40, + servingSize: '1 medium tomato', + caloriesPerServing: 25, + trivia: const [ + Trivia( + 'French speakers sometimes refer to tomatoes with which name?', + [ + 'Piet de terre', + 'Mille-feuille', + 'Pomme d\'amour', + ], + 2, + ), + Trivia( + 'The largest tomato known to have been grown weighed in at how many pounds?', + ['8', '10', '12'], + 0, + ), + Trivia( + 'Which country is the world\'s largest producer of tomatoes?', + [ + 'China', + 'Italy', + 'Ecuador', + ], + 0, + ), + ], + ), + Veggie( + id: 29, + name: 'Watermelon', + imageAssetPath: 'assets/images/watermelon.jpg', + category: VeggieCategory.melon, + shortDescription: 'Everyone\'s favorite closing act at the picnic.', + accentColor: const Color(0x40fa8c75), + seasons: [Season.summer], + vitaminAPercentage: 30, + vitaminCPercentage: 25, + servingSize: '2 cups diced', + caloriesPerServing: 80, + trivia: const [ + Trivia('How much of a watermelon is water?', [ + '50%', + '75%', + '90%', + ], 2), + Trivia( + 'Which nation is famous for growing watermelons in unsual shapes like cubes and hearts?', + ['Armenia', 'Japan', 'Saudi Arabia'], + 1, + ), + Trivia( + 'Which U.S. state declared the watermelon to be its state vegetable (that\'s right, vegetable)?', + ['Kansas', 'Iowa', 'Oklahoma'], + 2, + ), + Trivia( + 'Early explorers to the Americas used watermelons as which piece of equipment?', + ['Stools', 'Pillows', 'Canteens'], + 2, + ), + ], + ), + Veggie( + id: 30, + name: 'Orange Bell Pepper', + imageAssetPath: 'assets/images/orange_bell_pepper.jpg', + category: VeggieCategory.stealthFruit, + shortDescription: 'Like green pepper, but nicer.', + accentColor: const Color(0x40fd8e00), + seasons: [Season.summer], + vitaminAPercentage: 4, + vitaminCPercentage: 190, + servingSize: '1 medium pepper', + caloriesPerServing: 25, + trivia: const [ + Trivia( + 'Which compound (not found in bell peppers) is responsible for many peppers\' spicy taste?', + ['Alum', 'Capsacin', 'Calcium'], + 1, + ), + Trivia( + 'In comparison to green peppers, how expensive are their orange cousins?', + ['Cheaper', 'About the same', 'More expensive'], + 2, + ), + Trivia( + 'Who is generally credited with giving bell peppers their peppery name?', + [ + 'Christopher Columbus', + 'Benjamin Franklin', + 'Eleanor Roosevelt', + ], + 0, + ), + ], + ), + ]; +} diff --git a/veggieseasons/lib/data/preferences.dart b/veggieseasons/lib/data/preferences.dart new file mode 100644 index 0000000..07f16ca --- /dev/null +++ b/veggieseasons/lib/data/preferences.dart @@ -0,0 +1,90 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'veggie.dart'; + +/// A model class that mirrors the options in [SettingsScreen] and stores data +/// in shared preferences. +class Preferences extends ChangeNotifier { + // Keys to use with shared preferences. + static const _caloriesKey = 'calories'; + static const _preferredCategoriesKey = 'preferredCategories'; + + // Indicates whether a call to [_loadFromSharedPrefs] is in progress; + Future? _loading; + + int _desiredCalories = 2000; + + final Set _preferredCategories = {}; + + Future get desiredCalories async { + await _loading; + return _desiredCalories; + } + + Future> get preferredCategories async { + await _loading; + return Set.from(_preferredCategories); + } + + Future addPreferredCategory(VeggieCategory category) async { + _preferredCategories.add(category); + await _saveToSharedPrefs(); + notifyListeners(); + } + + Future removePreferredCategory(VeggieCategory category) async { + _preferredCategories.remove(category); + await _saveToSharedPrefs(); + notifyListeners(); + } + + Future setDesiredCalories(int calories) async { + _desiredCalories = calories; + await _saveToSharedPrefs(); + notifyListeners(); + } + + Future restoreDefaults() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + load(); + } + + void load() { + _loading = _loadFromSharedPrefs(); + } + + Future _saveToSharedPrefs() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_caloriesKey, _desiredCalories); + + // Store preferred categories as a comma-separated string containing their + // indices. + await prefs.setString( + _preferredCategoriesKey, + _preferredCategories.map((c) => c.index.toString()).join(','), + ); + } + + Future _loadFromSharedPrefs() async { + final prefs = await SharedPreferences.getInstance(); + _desiredCalories = prefs.getInt(_caloriesKey) ?? 2000; + _preferredCategories.clear(); + final names = prefs.getString(_preferredCategoriesKey); + + if (names != null && names.isNotEmpty) { + for (final name in names.split(',')) { + final index = int.tryParse(name) ?? -1; + _preferredCategories.add(VeggieCategory.values[index]); + } + } + + notifyListeners(); + } +} diff --git a/veggieseasons/lib/data/veggie.dart b/veggieseasons/lib/data/veggie.dart new file mode 100644 index 0000000..feeb83b --- /dev/null +++ b/veggieseasons/lib/data/veggie.dart @@ -0,0 +1,125 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; + +enum VeggieCategory { + allium, + berry, + citrus, + cruciferous, + fern, + flower, + fruit, + fungus, + gourd, + leafy, + legume, + melon, + root, + stealthFruit, + stoneFruit, + tropical, + tuber, + vegetable, +} + +enum Season { winter, spring, summer, autumn } + +class Trivia { + final String question; + final List answers; + final int correctAnswerIndex; + + const Trivia(this.question, this.answers, this.correctAnswerIndex); +} + +const Map veggieCategoryNames = { + VeggieCategory.allium: 'Allium', + VeggieCategory.berry: 'Berry', + VeggieCategory.citrus: 'Citrus', + VeggieCategory.cruciferous: 'Cruciferous', + VeggieCategory.fern: 'Technically a fern', + VeggieCategory.flower: 'Flower', + VeggieCategory.fruit: 'Fruit', + VeggieCategory.fungus: 'Fungus', + VeggieCategory.gourd: 'Gourd', + VeggieCategory.leafy: 'Leafy', + VeggieCategory.legume: 'Legume', + VeggieCategory.melon: 'Melon', + VeggieCategory.root: 'Root vegetable', + VeggieCategory.stealthFruit: 'Stealth fruit', + VeggieCategory.stoneFruit: 'Stone fruit', + VeggieCategory.tropical: 'Tropical', + VeggieCategory.tuber: 'Tuber', + VeggieCategory.vegetable: 'Vegetable', +}; + +const Map seasonNames = { + Season.winter: 'Winter', + Season.spring: 'Spring', + Season.summer: 'Summer', + Season.autumn: 'Autumn', +}; + +class Veggie { + Veggie({ + required this.id, + required this.name, + required this.imageAssetPath, + required this.category, + required this.shortDescription, + required this.accentColor, + required this.seasons, + required this.vitaminAPercentage, + required this.vitaminCPercentage, + required this.servingSize, + required this.caloriesPerServing, + required this.trivia, + this.isFavorite = false, + }); + + final int id; + + final String name; + + /// Each veggie has an associated image asset that's used as a background + /// image and icon. + final String imageAssetPath; + + final VeggieCategory category; + + /// A short, snappy line. + final String shortDescription; + + /// A color value to use when constructing UI elements to match the image + /// found at [imageAssetPath]. + final Color accentColor; + + /// Seasons during which a veggie is harvested. + final List seasons; + + /// Percentage of the FDA's recommended daily value of vitamin A for someone + /// with a 2,000 calorie diet. + final int vitaminAPercentage; + + /// Percentage of the FDA's recommended daily value of vitamin C for someone + /// with a 2,000 calorie diet. + final int vitaminCPercentage; + + /// A text description of a single serving (e.g. '1 apple' or '1/2 cup'). + final String servingSize; + + /// Calories per serving (as described in [servingSize]). + final int caloriesPerServing; + + /// Whether or not the veggie has been saved to the user's garden (i.e. marked + /// as a favorite). + bool isFavorite; + + /// A set of trivia questions and answers related to the veggie. + final List trivia; + + String? get categoryName => veggieCategoryNames[category]; +} diff --git a/veggieseasons/lib/main.dart b/veggieseasons/lib/main.dart new file mode 100644 index 0000000..13f321f --- /dev/null +++ b/veggieseasons/lib/main.dart @@ -0,0 +1,243 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/services.dart' + show DeviceOrientation, SystemChrome; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:window_size/window_size.dart'; + +import 'data/app_state.dart'; +import 'data/preferences.dart'; +import 'screens/details.dart'; +import 'screens/favorites.dart'; +import 'screens/home.dart'; +import 'screens/list.dart'; +import 'screens/search.dart'; +import 'screens/settings.dart'; +import 'styles.dart'; +import 'widgets/veggie_seasons_page.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + setupWindow(); + + runApp( + const RootRestorationScope(restorationId: 'root', child: VeggieApp()), + ); +} + +const double windowWidth = 480; +const double windowHeight = 854; + +void setupWindow() { + if (!kIsWeb && + (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { + setWindowTitle('Veggie Seasons'); + setWindowMinSize(const Size(windowWidth, windowHeight)); + setWindowMaxSize(const Size(windowWidth, windowHeight)); + getCurrentScreen().then((screen) { + setWindowFrame( + Rect.fromCenter( + center: screen!.frame.center, + width: windowWidth, + height: windowHeight, + ), + ); + }); + } +} + +final _rootNavigatorKey = GlobalKey(); +final _shellNavigatorKey = GlobalKey(); + +class VeggieApp extends StatefulWidget { + const VeggieApp({super.key}); + + @override + State createState() => _VeggieAppState(); +} + +class _VeggieAppState extends State with RestorationMixin { + final _RestorableAppState _appState = _RestorableAppState(); + + @override + String get restorationId => 'wrapper'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_appState, 'state'); + } + + @override + void dispose() { + _appState.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _appState.value), + ChangeNotifierProvider(create: (_) => Preferences()..load()), + ], + child: CupertinoApp.router( + theme: Styles.veggieThemeData, + debugShowCheckedModeBanner: false, + restorationScopeId: 'app', + routerConfig: GoRouter( + navigatorKey: _rootNavigatorKey, + restorationScopeId: 'router', + initialLocation: '/list', + redirect: (context, state) { + if (state.path == '/') { + return '/list'; + } + return null; + }, + debugLogDiagnostics: true, + routes: [ + ShellRoute( + navigatorKey: _shellNavigatorKey, + pageBuilder: (context, state, child) { + return CupertinoPage( + restorationId: 'router.shell', + child: HomeScreen( + restorationId: 'home', + child: child, + onTap: (index) { + if (index == 0) { + context.go('/list'); + } else if (index == 1) { + context.go('/favorites'); + } else if (index == 2) { + context.go('/search'); + } else { + context.go('/settings'); + } + }, + ), + ); + }, + routes: [ + GoRoute( + path: '/list', + pageBuilder: (context, state) { + return VeggieSeasonsPage( + key: state.pageKey, + restorationId: 'route.list', + child: const ListScreen(restorationId: 'list'), + ); + }, + routes: [_buildDetailsRoute()], + ), + GoRoute( + path: '/favorites', + pageBuilder: (context, state) { + return VeggieSeasonsPage( + key: state.pageKey, + restorationId: 'route.favorites', + child: const FavoritesScreen( + restorationId: 'favorites', + ), + ); + }, + routes: [_buildDetailsRoute()], + ), + GoRoute( + path: '/search', + pageBuilder: (context, state) { + return VeggieSeasonsPage( + key: state.pageKey, + restorationId: 'route.search', + child: const SearchScreen(restorationId: 'search'), + ); + }, + routes: [_buildDetailsRoute()], + ), + GoRoute( + path: '/settings', + pageBuilder: (context, state) { + return VeggieSeasonsPage( + key: state.pageKey, + restorationId: 'route.settings', + child: const SettingsScreen( + restorationId: 'settings', + ), + ); + }, + routes: [ + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + path: 'categories', + pageBuilder: (context, state) { + return VeggieCategorySettingsScreen.pageBuilder( + context, + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + path: 'calories', + pageBuilder: (context, state) { + return CalorieSettingsScreen.pageBuilder(context); + }, + ), + ], + ), + ], + ), + ], + ), + ), + ); + } + + // GoRouter does not support relative routes, + // see https://github.com/flutter/flutter/issues/108177 + GoRoute _buildDetailsRoute() { + return GoRoute( + parentNavigatorKey: _rootNavigatorKey, + path: 'details/:id', + pageBuilder: (context, state) { + final veggieId = int.parse(state.pathParameters['id']!); + return CupertinoPage( + restorationId: 'route.details', + child: DetailsScreen(id: veggieId, restorationId: 'details'), + ); + }, + ); + } +} + +class _RestorableAppState extends RestorableListenable { + @override + AppState createDefaultValue() { + return AppState(); + } + + @override + AppState fromPrimitives(Object? data) { + final appState = AppState(); + final favorites = (data as List).cast(); + for (var id in favorites) { + appState.setFavorite(id, true); + } + return appState; + } + + @override + Object toPrimitives() { + return value.favoriteVeggies.map((veggie) => veggie.id).toList(); + } +} diff --git a/veggieseasons/lib/screens/details.dart b/veggieseasons/lib/screens/details.dart new file mode 100644 index 0000000..90c3aa0 --- /dev/null +++ b/veggieseasons/lib/screens/details.dart @@ -0,0 +1,294 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import '../data/app_state.dart'; +import '../data/preferences.dart'; +import '../data/veggie.dart'; +import '../styles.dart'; +import '../widgets/detail_buttons.dart'; + +class ServingInfoChart extends StatelessWidget { + const ServingInfoChart(this.veggie, this.prefs, {super.key}); + + final Veggie veggie; + + final Preferences prefs; + + // Creates a [Text] widget to display a veggie's "percentage of your daily + // value of this vitamin" data adjusted for the user's preferred calorie + // target. + Widget _buildVitaminText( + int standardPercentage, + Future targetCalories, + ) { + return FutureBuilder( + future: targetCalories, + builder: (context, snapshot) { + final target = snapshot.data ?? 2000; + final percent = standardPercentage * 2000 ~/ target; + + return Text( + '$percent% DV', + style: CupertinoTheme.of(context).textTheme.textStyle, + textAlign: TextAlign.end, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final themeData = CupertinoTheme.of(context); + return Column( + children: [ + const SizedBox(height: 32), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text( + 'Serving size', + style: Styles.detailsServingLabelText(themeData), + ), + const Spacer(), + Text( + veggie.servingSize, + textAlign: TextAlign.end, + style: CupertinoTheme.of(context).textTheme.textStyle, + ), + ], + ), + const SizedBox(height: 24), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text( + 'Calories', + style: Styles.detailsServingLabelText(themeData), + ), + const Spacer(), + Text( + '${veggie.caloriesPerServing} kCal', + style: CupertinoTheme.of(context).textTheme.textStyle, + textAlign: TextAlign.end, + ), + ], + ), + const SizedBox(height: 24), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text( + 'Vitamin A', + style: Styles.detailsServingLabelText(themeData), + ), + const Spacer(), + _buildVitaminText( + veggie.vitaminAPercentage, + prefs.desiredCalories, + ), + ], + ), + const SizedBox(height: 24), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text( + 'Vitamin C', + style: Styles.detailsServingLabelText(themeData), + ), + const Spacer(), + _buildVitaminText( + veggie.vitaminCPercentage, + prefs.desiredCalories, + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 32), + child: FutureBuilder( + future: prefs.desiredCalories, + builder: (context, snapshot) { + return Text( + 'Percent daily values based on a diet of ' + '${snapshot.data ?? '2,000'} calories.', + style: Styles.detailsServingNoteText(themeData), + ); + }, + ), + ), + ], + ); + } +} + +class InfoView extends StatelessWidget { + final int? id; + + const InfoView(this.id, {super.key}); + + @override + Widget build(BuildContext context) { + final appState = Provider.of(context); + final prefs = Provider.of(context); + final veggie = appState.getVeggie(id); + final themeData = CupertinoTheme.of(context); + + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(veggie.name, style: Styles.detailsTitleText(themeData)), + const SizedBox(height: 8), + Text( + veggie.shortDescription, + style: CupertinoTheme.of(context).textTheme.textStyle, + ), + const SizedBox(height: 16), + Text( + 'Seasons', + style: Styles.detailsServingLabelText(themeData), + ), + const SizedBox(height: 12), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + for (var season in Season.values) ...[ + const Spacer(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Styles.seasonIconData[season], + color: veggie.seasons.contains(season) + ? Styles.seasonColors[season] + : const Color.fromRGBO(128, 128, 128, 1), + size: 24, + ), + const SizedBox(height: 4), + Text( + season.name.characters.first.toUpperCase() + + season.name.characters.skip(1).string, + style: Styles.minorText( + CupertinoTheme.of(context), + ).copyWith(fontSize: 11), + ), + ], + ), + const Spacer(), + ], + ], + ), + ServingInfoChart(veggie, prefs), + ], + ), + ); + } +} + +class DetailsScreen extends StatelessWidget { + final int? id; + final String? restorationId; + + const DetailsScreen({this.id, this.restorationId, super.key}); + + Widget _buildHeader(BuildContext context, AppState model) { + final veggie = model.getVeggie(id); + + return SizedBox( + height: 240, + child: Stack( + children: [ + Positioned( + right: 0, + left: 0, + child: Image.asset( + veggie.imageAssetPath, + fit: BoxFit.cover, + semanticLabel: 'A background image of ${veggie.name}', + ), + ), + Positioned( + top: 16, + left: 16, + child: SafeArea( + child: CloseButton(() { + context.pop(); + }), + ), + ), + Positioned( + top: 16, + right: 16, + child: SafeArea( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShareButton(() { + showCupertinoModalPopup( + context: context, + builder: (context) { + return CupertinoActionSheet( + title: Text('Share ${veggie.name}'), + message: Text(veggie.shortDescription), + actions: [ + CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + }), + const SizedBox(width: 8), + Builder( + builder: (context) { + final appState = Provider.of(context); + final veggie = appState.getVeggie(id); + + return FavoriteButton( + () => appState.setFavorite(id, !veggie.isFavorite), + veggie.isFavorite, + ); + }, + ), + ], + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final appState = Provider.of(context); + + return CupertinoPageScaffold( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: ListView( + restorationId: 'list', + children: [ + _buildHeader(context, appState), + const SizedBox(height: 20), + InfoView(id), + ], + ), + ), + ], + ), + ); + } +} diff --git a/veggieseasons/lib/screens/favorites.dart b/veggieseasons/lib/screens/favorites.dart new file mode 100644 index 0000000..3bdf2b0 --- /dev/null +++ b/veggieseasons/lib/screens/favorites.dart @@ -0,0 +1,59 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; +import '../data/app_state.dart'; +import '../data/veggie.dart'; +import '../widgets/veggie_headline.dart'; + +class FavoritesScreen extends StatelessWidget { + const FavoritesScreen({this.restorationId, super.key}); + + final String? restorationId; + + @override + Widget build(BuildContext context) { + return CupertinoTabView( + restorationScopeId: restorationId, + builder: (context) { + final model = Provider.of(context); + + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('My Garden'), + ), + child: Center( + child: model.favoriteVeggies.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + 'You haven\'t added any favorite veggies to your garden yet.', + style: CupertinoTheme.of( + context, + ).textTheme.textStyle, + ), + ) + : ListView( + restorationId: 'list', + children: [ + const SizedBox(height: 24), + for (Veggie veggie in model.favoriteVeggies) + Padding( + padding: const EdgeInsets.fromLTRB( + 16, + 0, + 16, + 24, + ), + child: VeggieHeadline(veggie), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/veggieseasons/lib/screens/home.dart b/veggieseasons/lib/screens/home.dart new file mode 100644 index 0000000..fd46ba9 --- /dev/null +++ b/veggieseasons/lib/screens/home.dart @@ -0,0 +1,81 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +const _bottomNavigationBarItemIconPadding = EdgeInsets.only(top: 4.0); + +class HomeScreen extends StatelessWidget { + const HomeScreen({ + super.key, + this.restorationId, + required this.child, + required this.onTap, + }); + + final String? restorationId; + final Widget child; + final void Function(int) onTap; + + @override + Widget build(BuildContext context) { + final String location = GoRouter.of( + context, + ).routerDelegate.currentConfiguration.uri.toString(); + final index = _getSelectedIndex(location); + return RestorationScope( + restorationId: restorationId, + child: CupertinoPageScaffold( + child: Column( + children: [ + Expanded(child: child), + CupertinoTabBar( + currentIndex: index, + items: const [ + BottomNavigationBarItem( + icon: Padding( + padding: _bottomNavigationBarItemIconPadding, + child: Icon(CupertinoIcons.home), + ), + label: 'Home', + ), + BottomNavigationBarItem( + icon: Padding( + padding: _bottomNavigationBarItemIconPadding, + child: Icon(CupertinoIcons.book), + ), + label: 'My Garden', + ), + BottomNavigationBarItem( + icon: Padding( + padding: _bottomNavigationBarItemIconPadding, + child: Icon(CupertinoIcons.search), + ), + label: 'Search', + ), + BottomNavigationBarItem( + icon: Padding( + padding: _bottomNavigationBarItemIconPadding, + child: Icon(CupertinoIcons.settings), + ), + label: 'Settings', + ), + ], + onTap: onTap, + ), + ], + ), + ), + ); + } + + int _getSelectedIndex(String location) { + if (location.startsWith('/list')) return 0; + if (location.startsWith('/favorites')) return 1; + if (location.startsWith('/search')) return 2; + if (location.startsWith('/settings')) return 3; + return 0; + } +} diff --git a/veggieseasons/lib/screens/list.dart b/veggieseasons/lib/screens/list.dart new file mode 100644 index 0000000..c348322 --- /dev/null +++ b/veggieseasons/lib/screens/list.dart @@ -0,0 +1,95 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import '../data/app_state.dart'; +import '../data/preferences.dart'; +import '../data/veggie.dart'; +import '../styles.dart'; +import '../widgets/veggie_card.dart'; + +class ListScreen extends StatelessWidget { + const ListScreen({this.restorationId, super.key}); + + final String? restorationId; + + Widget _generateVeggieCard( + Veggie veggie, + Preferences prefs, { + bool inSeason = true, + }) { + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24), + child: FutureBuilder>( + future: prefs.preferredCategories, + builder: (context, snapshot) { + final data = snapshot.data ?? {}; + return VeggieCard( + veggie, + inSeason, + data.contains(veggie.category), + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return CupertinoTabView( + restorationScopeId: restorationId, + builder: (context) { + final appState = Provider.of(context); + final prefs = Provider.of(context); + final themeData = CupertinoTheme.of(context); + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarBrightness: MediaQuery.platformBrightnessOf(context), + ), + child: SafeArea( + bottom: false, + child: ListView.builder( + restorationId: 'list', + itemCount: appState.allVeggies.length + 2, + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), + child: Text( + 'In season today', + style: Styles.headlineText(themeData), + ), + ); + } else if (index <= appState.availableVeggies.length) { + return _generateVeggieCard( + appState.availableVeggies[index - 1], + prefs, + ); + } else if (index <= appState.availableVeggies.length + 1) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), + child: Text( + 'Not in season', + style: Styles.headlineText(themeData), + ), + ); + } else { + var relativeIndex = + index - (appState.availableVeggies.length + 2); + return _generateVeggieCard( + appState.unavailableVeggies[relativeIndex], + prefs, + inSeason: false, + ); + } + }, + ), + ), + ); + }, + ); + } +} diff --git a/veggieseasons/lib/screens/search.dart b/veggieseasons/lib/screens/search.dart new file mode 100644 index 0000000..824c6df --- /dev/null +++ b/veggieseasons/lib/screens/search.dart @@ -0,0 +1,132 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import '../data/app_state.dart'; +import '../data/veggie.dart'; +import '../widgets/veggie_headline.dart'; + +class SearchScreen extends StatefulWidget { + const SearchScreen({this.restorationId, super.key}); + + final String? restorationId; + + @override + State createState() => _SearchScreenState(); +} + +class _SearchScreenState extends State + with RestorationMixin { + final controller = RestorableTextEditingController(); + final focusNode = FocusNode(); + String? terms; + + @override + String? get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(controller, 'text'); + controller.addListener(_onTextChanged); + terms = controller.value.text; + } + + @override + void dispose() { + focusNode.dispose(); + controller.dispose(); + super.dispose(); + } + + void _onTextChanged() { + setState(() => terms = controller.value.text); + } + + Widget _createSearchBox({bool focus = true}) { + return Padding( + padding: const EdgeInsets.all(8), + child: CupertinoSearchTextField( + controller: controller.value, + focusNode: focus ? focusNode : null, + ), + ); + } + + Widget _buildSearchResults(List veggies) { + if (veggies.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + 'No veggies matching your search terms were found.', + style: CupertinoTheme.of(context).textTheme.textStyle, + ), + ), + ); + } + + return ListView.builder( + restorationId: 'list', + itemCount: veggies.length + 1, + itemBuilder: (context, i) { + if (i == 0) { + return Visibility( + // This invisible and otherwise unnecessary search box is used to + // pad the list entries downward, so none will be underneath the + // real search box when the list is at its top scroll position. + visible: false, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + // This invisible and otherwise unnecessary search box is used to + // pad the list entries downward, so none will be underneath the + // real search box when the list is at its top scroll position. + child: _createSearchBox(focus: false), + ); + } else { + return Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 24, + ), + child: VeggieHeadline(veggies[i - 1]), + ); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + final model = Provider.of(context); + + return UnmanagedRestorationScope( + bucket: bucket, + child: CupertinoTabView( + restorationScopeId: 'tabview', + builder: (context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarBrightness: MediaQuery.platformBrightnessOf( + context, + ), + ), + child: SafeArea( + bottom: false, + child: Stack( + children: [ + _buildSearchResults(model.searchVeggies(terms)), + _createSearchBox(), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/veggieseasons/lib/screens/settings.dart b/veggieseasons/lib/screens/settings.dart new file mode 100644 index 0000000..eb537dc --- /dev/null +++ b/veggieseasons/lib/screens/settings.dart @@ -0,0 +1,311 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import '../data/preferences.dart'; +import '../data/veggie.dart'; +import '../styles.dart'; + +class VeggieCategorySettingsScreen extends StatelessWidget { + const VeggieCategorySettingsScreen({super.key, this.restorationId}); + + final String? restorationId; + + static Page pageBuilder(BuildContext context) { + return const CupertinoPage( + restorationId: 'router.categories', + child: VeggieCategorySettingsScreen(restorationId: 'category'), + title: 'Preferred Categories', + ); + } + + @override + Widget build(BuildContext context) { + final model = Provider.of(context); + final currentPrefs = model.preferredCategories; + var brightness = CupertinoTheme.brightnessOf(context); + return RestorationScope( + restorationId: restorationId, + child: CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('Preferred Categories'), + previousPageTitle: 'Settings', + ), + backgroundColor: Styles.scaffoldBackground(brightness), + child: FutureBuilder>( + future: currentPrefs, + builder: (context, snapshot) { + final tiles = []; + + for (final category in VeggieCategory.values) { + CupertinoSwitch toggle; + + // It's possible that category data hasn't loaded from shared prefs + // yet, so display it if possible and fall back to disabled switches + // otherwise. + if (snapshot.hasData) { + toggle = CupertinoSwitch( + value: snapshot.data!.contains(category), + onChanged: (value) { + if (value) { + model.addPreferredCategory(category); + } else { + model.removePreferredCategory(category); + } + }, + ); + } else { + toggle = const CupertinoSwitch( + value: false, + onChanged: null, + ); + } + + tiles.add( + CupertinoListTile.notched( + title: Text(veggieCategoryNames[category]!), + trailing: toggle, + ), + ); + } + + return ListView( + restorationId: 'list', + children: [ + CupertinoListSection.insetGrouped( + hasLeading: false, + children: tiles, + ), + ], + ); + }, + ), + ), + ); + } +} + +class CalorieSettingsScreen extends StatelessWidget { + const CalorieSettingsScreen({super.key, this.restorationId}); + + final String? restorationId; + + static const max = 1000; + static const min = 2600; + static const step = 200; + + static Page pageBuilder(BuildContext context) { + return const CupertinoPage( + restorationId: 'router.calorie', + child: CalorieSettingsScreen(restorationId: 'calorie'), + title: 'Calorie Target', + ); + } + + @override + Widget build(BuildContext context) { + final model = Provider.of(context); + var brightness = CupertinoTheme.brightnessOf(context); + return RestorationScope( + restorationId: restorationId, + child: CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + previousPageTitle: 'Settings', + ), + backgroundColor: Styles.scaffoldBackground(brightness), + child: ListView( + restorationId: 'list', + children: [ + FutureBuilder( + future: model.desiredCalories, + builder: (context, snapshot) { + final tiles = []; + + for (var cals = max; cals < min; cals += step) { + tiles.add( + CupertinoListTile.notched( + title: Text('$cals calories'), + trailing: SettingsIcon( + icon: CupertinoIcons.check_mark, + foregroundColor: + snapshot.hasData && snapshot.data == cals + ? CupertinoColors.activeBlue + : Styles.transparentColor, + backgroundColor: Styles.transparentColor, + ), + onTap: snapshot.hasData + ? () => model.setDesiredCalories(cals) + : null, + ), + ); + } + + return CupertinoListSection.insetGrouped( + header: Text( + 'Available calorie levels'.toUpperCase(), + style: Styles.settingsGroupHeaderText( + CupertinoTheme.of(context), + ), + ), + footer: Text( + 'These are used for serving calculations', + style: Styles.settingsGroupFooterText( + CupertinoTheme.of(context), + ), + ), + children: tiles, + ); + }, + ), + ], + ), + ), + ); + } +} + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({this.restorationId, super.key}); + + final String? restorationId; + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + CupertinoListTile _buildCaloriesTile( + BuildContext context, + Preferences prefs, + ) { + return CupertinoListTile.notched( + leading: const SettingsIcon( + backgroundColor: CupertinoColors.systemBlue, + icon: Styles.calorieIcon, + ), + title: const Text('Calorie Target'), + additionalInfo: FutureBuilder( + future: prefs.desiredCalories, + builder: (context, snapshot) { + return Text( + snapshot.data?.toString() ?? '', + style: CupertinoTheme.of(context).textTheme.textStyle, + ); + }, + ), + trailing: const CupertinoListTileChevron(), + onTap: () => context.go('/settings/calories'), + ); + } + + CupertinoListTile _buildCategoriesTile( + BuildContext context, + Preferences prefs, + ) { + return CupertinoListTile.notched( + leading: const SettingsIcon( + backgroundColor: CupertinoColors.systemOrange, + icon: Styles.preferenceIcon, + ), + title: const Text('Preferred Categories'), + trailing: const CupertinoListTileChevron(), + onTap: () => context.go('/settings/categories'), + ); + } + + CupertinoListTile _buildRestoreDefaultsTile( + BuildContext context, + Preferences prefs, + ) { + return CupertinoListTile.notched( + leading: const SettingsIcon( + backgroundColor: CupertinoColors.systemRed, + icon: Styles.resetIcon, + ), + title: const Text('Restore Defaults'), + onTap: () { + showCupertinoDialog( + context: context, + builder: (context) => CupertinoAlertDialog( + title: const Text('Are you sure?'), + content: const Text( + 'Are you sure you want to reset the current settings?', + ), + actions: [ + CupertinoDialogAction( + isDestructiveAction: true, + child: const Text('Yes'), + onPressed: () async { + await prefs.restoreDefaults(); + if (!context.mounted) return; + context.pop(); + }, + ), + CupertinoDialogAction( + isDefaultAction: true, + child: const Text('No'), + onPressed: () => context.pop(), + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final prefs = Provider.of(context); + + return CupertinoPageScaffold( + backgroundColor: Styles.scaffoldBackground( + CupertinoTheme.brightnessOf(context), + ), + child: CustomScrollView( + slivers: [ + const CupertinoSliverNavigationBar(largeTitle: Text('Settings')), + SliverList( + delegate: SliverChildListDelegate([ + CupertinoListSection.insetGrouped( + children: [ + _buildCaloriesTile(context, prefs), + _buildCategoriesTile(context, prefs), + ], + ), + CupertinoListSection.insetGrouped( + children: [_buildRestoreDefaultsTile(context, prefs)], + ), + ]), + ), + ], + ), + ); + } +} + +class SettingsIcon extends StatelessWidget { + const SettingsIcon({ + required this.icon, + this.foregroundColor = CupertinoColors.white, + this.backgroundColor = CupertinoColors.black, + super.key, + }); + + final Color backgroundColor; + final Color foregroundColor; + final IconData icon; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: backgroundColor, + ), + child: Center(child: Icon(icon, color: foregroundColor, size: 20)), + ); + } +} diff --git a/veggieseasons/lib/styles.dart b/veggieseasons/lib/styles.dart new file mode 100644 index 0000000..ce45247 --- /dev/null +++ b/veggieseasons/lib/styles.dart @@ -0,0 +1,221 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'data/veggie.dart'; + +abstract class Styles { + static CupertinoThemeData veggieThemeData = const CupertinoThemeData( + textTheme: CupertinoTextThemeData( + textStyle: TextStyle( + color: CupertinoColors.label, + fontSize: 16, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + fontFamily: 'CupertinoSystemText', + letterSpacing: -0.41, + decoration: TextDecoration.none, + ), + ), + ); + + static TextStyle headlineText(CupertinoThemeData themeData) => themeData + .textTheme + .textStyle + .copyWith(fontSize: 32, fontWeight: FontWeight.bold); + + static TextStyle minorText(CupertinoThemeData themeData) => themeData + .textTheme + .textStyle + .copyWith(color: const Color.fromRGBO(128, 128, 128, 1)); + + static TextStyle headlineName(CupertinoThemeData themeData) => themeData + .textTheme + .textStyle + .copyWith(fontSize: 24, fontWeight: FontWeight.bold); + + static TextStyle cardTitleText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith( + color: const Color.fromRGBO(0, 0, 0, 0.9), + fontSize: 32, + fontWeight: FontWeight.bold, + ); + + static TextStyle cardCategoryText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith( + color: const Color.fromRGBO(255, 255, 255, 0.9), + ); + + static TextStyle cardDescriptionText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith( + color: const Color.fromRGBO(0, 0, 0, 0.9), + ); + + static TextStyle detailsTitleText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith( + fontSize: 30, + fontWeight: FontWeight.bold, + ); + + static TextStyle detailsPreferredCategoryText( + CupertinoThemeData themeData, + ) => themeData.textTheme.textStyle.copyWith(fontWeight: FontWeight.bold); + + static TextStyle detailsBoldDescriptionText( + CupertinoThemeData themeData, + ) => themeData.textTheme.textStyle.copyWith( + color: const Color.fromRGBO(0, 0, 0, 0.9), + fontWeight: FontWeight.bold, + ); + + static TextStyle detailsServingHeaderText( + CupertinoThemeData themeData, + ) => themeData.textTheme.textStyle.copyWith( + color: const Color.fromRGBO(176, 176, 176, 1), + fontWeight: FontWeight.bold, + ); + + static TextStyle detailsServingLabelText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith(fontWeight: FontWeight.bold); + + static TextStyle detailsServingNoteText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith(fontStyle: FontStyle.italic); + + static TextStyle triviaFinishedTitleText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith(fontSize: 32); + + static TextStyle triviaFinishedBigText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith(fontSize: 48); + + static TextStyle settingsGroupHeaderText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith( + color: CupertinoColors.inactiveGray, + fontSize: 13.5, + letterSpacing: -0.5, + ); + + static TextStyle settingsGroupFooterText(CupertinoThemeData themeData) => + themeData.textTheme.textStyle.copyWith( + color: const Color(0xff777777), + fontSize: 13, + letterSpacing: -0.08, + ); + + static const appBackground = Color(0xffd0d0d0); + + static Color? scaffoldBackground(Brightness brightness) => + brightness == Brightness.light + ? CupertinoColors.extraLightBackgroundGray + : null; + + static const frostedBackground = Color(0xccf8f8f8); + + static const closeButtonUnpressed = Color(0xff101010); + + static const closeButtonPressed = Color(0xff808080); + + static TextStyle settingsItemSubtitleText( + CupertinoThemeData themeData, + ) => themeData.textTheme.textStyle.copyWith( + fontSize: 12, + letterSpacing: -0.2, + ); + + static const Color searchCursorColor = Color.fromRGBO(0, 122, 255, 1); + + static const Color searchIconColor = Color.fromRGBO(128, 128, 128, 1); + + static const seasonColors = { + Season.winter: Color(0xff336dcc), + Season.spring: Color(0xff2fa02b), + Season.summer: Color(0xff287213), + Season.autumn: Color(0xff724913), + }; + + // While handy, some of the Font Awesome icons sometimes bleed over their + // allotted bounds. This padding is used to adjust for that. + static const seasonIconPadding = { + Season.winter: EdgeInsets.only(right: 0), + Season.spring: EdgeInsets.only(right: 4), + Season.summer: EdgeInsets.only(right: 6), + Season.autumn: EdgeInsets.only(right: 0), + }; + + static const seasonIconData = { + Season.winter: FontAwesomeIcons.snowflake, + Season.spring: FontAwesomeIcons.leaf, + Season.summer: FontAwesomeIcons.umbrellaBeach, + Season.autumn: FontAwesomeIcons.canadianMapleLeaf, + }; + + static const seasonBorder = Border( + top: BorderSide(color: Color(0xff606060)), + left: BorderSide(color: Color(0xff606060)), + bottom: BorderSide(color: Color(0xff606060)), + right: BorderSide(color: Color(0xff606060)), + ); + + static const uncheckedIcon = IconData( + 0xf372, + fontFamily: CupertinoIcons.iconFont, + fontPackage: CupertinoIcons.iconFontPackage, + ); + + static const checkedIcon = IconData( + 0xf373, + fontFamily: CupertinoIcons.iconFont, + fontPackage: CupertinoIcons.iconFontPackage, + ); + + static const transparentColor = Color(0x00000000); + + static const shadowColor = Color(0xa0000000); + + static const shadowGradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [transparentColor, shadowColor], + ); + + static const Color settingsMediumGray = Color(0xffc7c7c7); + + static const Color settingsItemPressed = Color(0xffd9d9d9); + + static Color settingsItemColor(Brightness brightness) => + brightness == Brightness.light + ? CupertinoColors.tertiarySystemBackground + : CupertinoColors.darkBackgroundGray; + + static Color settingsLineation(Brightness brightness) => + brightness == Brightness.light + ? const Color(0xffbcbbc1) + : const Color(0xff4c4b4b); + + static const Color settingsBackground = Color(0xffefeff4); + + static const preferenceIcon = IconData( + 0xf443, + fontFamily: CupertinoIcons.iconFont, + fontPackage: CupertinoIcons.iconFontPackage, + ); + + static const resetIcon = IconData( + 0xf4c4, + fontFamily: CupertinoIcons.iconFont, + fontPackage: CupertinoIcons.iconFontPackage, + ); + + static const calorieIcon = IconData( + 0xf3bb, + fontFamily: CupertinoIcons.iconFont, + fontPackage: CupertinoIcons.iconFontPackage, + ); + + static const servingInfoBorderColor = Color(0xffb0b0b0); + + static const ColorFilter desaturatedColorFilter = + // 222222 is a random color that has low color saturation. + ColorFilter.mode(Color(0xff222222), BlendMode.saturation); +} diff --git a/veggieseasons/lib/widgets/detail_buttons.dart b/veggieseasons/lib/widgets/detail_buttons.dart new file mode 100644 index 0000000..1ad4d1e --- /dev/null +++ b/veggieseasons/lib/widgets/detail_buttons.dart @@ -0,0 +1,148 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import '../styles.dart'; + +/// Partially overlays and then blurs its child's background. +class FrostedBox extends StatelessWidget { + const FrostedBox({this.child, super.key}); + + final Widget? child; + + @override + Widget build(BuildContext context) { + return BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: DecoratedBox( + decoration: const BoxDecoration(color: Styles.frostedBackground), + child: child, + ), + ); + } +} + +/// An Icon that implicitly animates changes to its color. +class ColorChangingIcon extends ImplicitlyAnimatedWidget { + const ColorChangingIcon( + this.icon, { + this.color = CupertinoColors.black, + this.size, + required super.duration, + super.key, + }); + + final Color color; + + final IconData icon; + + final double? size; + + @override + AnimatedWidgetBaseState createState() => + _ColorChangingIconState(); +} + +class _ColorChangingIconState + extends AnimatedWidgetBaseState { + ColorTween? _colorTween; + + @override + Widget build(BuildContext context) { + return Icon( + widget.icon, + semanticLabel: 'Close button', + size: widget.size, + color: _colorTween?.evaluate(animation), + ); + } + + @override + void forEachTween(TweenVisitor visitor) { + _colorTween = + visitor( + _colorTween, + widget.color, + (dynamic value) => ColorTween(begin: value as Color?), + ) + as ColorTween?; + } +} + +/// A close button that invokes a callback when pressed. +class CloseButton extends _DetailPageButton { + const CloseButton(VoidCallback onPressed, {super.key}) + : super(onPressed, CupertinoIcons.chevron_back); +} + +/// A share button that invokes a callback when pressed. +class ShareButton extends _DetailPageButton { + const ShareButton(VoidCallback onPressed, {super.key}) + : super(onPressed, CupertinoIcons.share); +} + +/// A favorite button that invokes a callback when pressed. +class FavoriteButton extends _DetailPageButton { + const FavoriteButton( + VoidCallback onPressed, + bool isFavorite, { + super.key, + }) : super( + onPressed, + isFavorite ? CupertinoIcons.heart_fill : CupertinoIcons.heart, + ); +} + +class _DetailPageButton extends StatefulWidget { + const _DetailPageButton(this.onPressed, this.icon, {super.key}); + + final VoidCallback onPressed; + final IconData icon; + + @override + State<_DetailPageButton> createState() => _DetailPageButtonState(); +} + +class _DetailPageButtonState extends State<_DetailPageButton> { + bool tapInProgress = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (details) { + setState(() => tapInProgress = true); + }, + onTapUp: (details) { + setState(() => tapInProgress = false); + widget.onPressed(); + }, + onTapCancel: () { + setState(() => tapInProgress = false); + }, + child: ClipOval( + child: FrostedBox( + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + ), + child: Center( + child: ColorChangingIcon( + widget.icon, + duration: const Duration(milliseconds: 300), + color: tapInProgress + ? Styles.closeButtonPressed + : Styles.closeButtonUnpressed, + size: 20, + ), + ), + ), + ), + ), + ); + } +} diff --git a/veggieseasons/lib/widgets/veggie_card.dart b/veggieseasons/lib/widgets/veggie_card.dart new file mode 100644 index 0000000..ce7e537 --- /dev/null +++ b/veggieseasons/lib/widgets/veggie_card.dart @@ -0,0 +1,114 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../data/veggie.dart'; +import '../styles.dart'; + +/// A Card-like Widget that responds to tap events by animating changes to its +/// elevation and invoking an optional [onPressed] callback. +class PressableCard extends StatelessWidget { + const PressableCard({ + required this.child, + this.borderRadius = const BorderRadius.all(Radius.circular(16)), + this.onPressed, + super.key, + }); + + final VoidCallback? onPressed; + + final Widget child; + + final BorderRadius borderRadius; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + child: Container( + decoration: BoxDecoration(borderRadius: borderRadius), + child: ClipRRect(borderRadius: borderRadius, child: child), + ), + ); + } +} + +class VeggieCard extends StatelessWidget { + const VeggieCard( + this.veggie, + this.isInSeason, + this.isPreferredCategory, { + super.key, + }); + + /// Veggie to be displayed by the card. + final Veggie veggie; + + /// If the veggie is in season, it's displayed more prominently and the + /// image is fully saturated. Otherwise, it's reduced and de-saturated. + final bool isInSeason; + + /// Whether [veggie] falls into one of user's preferred [VeggieCategory]s + final bool isPreferredCategory; + + Widget _buildDetails(BuildContext context) { + final themeData = CupertinoTheme.of(context); + return Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 16, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(veggie.name, style: Styles.cardTitleText(themeData)), + const SizedBox(height: 8), + Text( + veggie.shortDescription, + style: Styles.cardDescriptionText(themeData), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return PressableCard( + onPressed: () { + // GoRouter does not support relative routes, + // so navigate to the absolute route. + // see https://github.com/flutter/flutter/issues/108177 + context.go('/list/details/${veggie.id}'); + }, + child: Stack( + children: [ + Semantics( + label: 'A card background featuring ${veggie.name}', + child: Container( + height: isInSeason ? 300 : 150, + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + colorFilter: isInSeason + ? null + : Styles.desaturatedColorFilter, + image: AssetImage(veggie.imageAssetPath), + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: _buildDetails(context), + ), + ], + ), + ); + } +} diff --git a/veggieseasons/lib/widgets/veggie_headline.dart b/veggieseasons/lib/widgets/veggie_headline.dart new file mode 100644 index 0000000..d21cf01 --- /dev/null +++ b/veggieseasons/lib/widgets/veggie_headline.dart @@ -0,0 +1,117 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; +import '../data/veggie.dart'; +import '../styles.dart'; + +class ZoomClipAssetImage extends StatelessWidget { + const ZoomClipAssetImage({ + required this.zoom, + this.height, + this.width, + required this.imageAsset, + super.key, + }); + + final double zoom; + final double? height; + final double? width; + final String imageAsset; + + @override + Widget build(BuildContext context) { + return Container( + height: height, + width: width, + alignment: Alignment.center, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: OverflowBox( + maxHeight: height! * zoom, + maxWidth: width! * zoom, + child: Image.asset(imageAsset, fit: BoxFit.fill), + ), + ), + ); + } +} + +class VeggieHeadline extends StatelessWidget { + final Veggie veggie; + + const VeggieHeadline(this.veggie, {super.key}); + + List _buildSeasonDots(List seasons) { + var widgets = []; + + for (var season in seasons) { + widgets.add(const SizedBox(width: 4)); + widgets.add( + Container( + height: 10, + width: 10, + decoration: BoxDecoration( + color: Styles.seasonColors[season], + borderRadius: BorderRadius.circular(5), + ), + ), + ); + } + + return widgets; + } + + @override + Widget build(BuildContext context) { + final themeData = CupertinoTheme.of(context); + final String location = GoRouter.of( + context, + ).routerDelegate.currentConfiguration.uri.toString(); + + return GestureDetector( + onTap: () { + // GoRouter does not support relative routes, + // so navigate to the absolute route, which can be either + // `/favorites/details/${veggie.id}` or `/search/details/${veggie.id}` + // see https://github.com/flutter/flutter/issues/108177 + context.go('$location/details/${veggie.id}'); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ZoomClipAssetImage( + imageAsset: veggie.imageAssetPath, + zoom: 2.4, + height: 72, + width: 72, + ), + const SizedBox(width: 8), + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + veggie.name, + style: Styles.headlineName(themeData), + ), + ..._buildSeasonDots(veggie.seasons), + ], + ), + Text( + veggie.shortDescription, + style: themeData.textTheme.textStyle, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/veggieseasons/lib/widgets/veggie_seasons_page.dart b/veggieseasons/lib/widgets/veggie_seasons_page.dart new file mode 100644 index 0000000..aac37df --- /dev/null +++ b/veggieseasons/lib/widgets/veggie_seasons_page.dart @@ -0,0 +1,45 @@ +// Copyright 2024, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; + +class VeggieSeasonsPage extends Page { + final Widget child; + + const VeggieSeasonsPage({ + super.key, + required this.child, + super.restorationId, + }); + + @override + VeggieSeasonsPageRoute createRoute(BuildContext context) => + VeggieSeasonsPageRoute(this); +} + +class VeggieSeasonsPageRoute extends PageRoute { + VeggieSeasonsPageRoute(VeggieSeasonsPage page) + : super(settings: page); + + VeggieSeasonsPage get _page => settings as VeggieSeasonsPage; + + @override + Color? get barrierColor => null; + + @override + String? get barrierLabel => null; + + @override + bool get maintainState => true; + + @override + Duration get transitionDuration => Duration.zero; + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => _page.child; +} diff --git a/veggieseasons/macos/.gitignore b/veggieseasons/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/veggieseasons/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/veggieseasons/macos/Flutter/Flutter-Debug.xcconfig b/veggieseasons/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/veggieseasons/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/veggieseasons/macos/Flutter/Flutter-Release.xcconfig b/veggieseasons/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/veggieseasons/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/veggieseasons/macos/Flutter/GeneratedPluginRegistrant.swift b/veggieseasons/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..f717508 --- /dev/null +++ b/veggieseasons/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import shared_preferences_foundation +import window_size + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin")) +} diff --git a/veggieseasons/macos/Podfile b/veggieseasons/macos/Podfile new file mode 100644 index 0000000..c795730 --- /dev/null +++ b/veggieseasons/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_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_macos_build_settings(target) + end +end diff --git a/veggieseasons/macos/Runner.xcodeproj/project.pbxproj b/veggieseasons/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3beb548 --- /dev/null +++ b/veggieseasons/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 0C591566BA30BFA6D780386A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4EE22A708C4E7F94406AFD15 /* Pods_Runner.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + AEA6F424A1A42303F9A597C0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBED630FEBB9A18F27C8E814 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* veggieseasons.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = veggieseasons.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 413A4F2B0013AFEC349AB2C5 /* 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 = ""; }; + 4D8AF33CD5C878ED37254D67 /* 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 = ""; }; + 4EE22A708C4E7F94406AFD15 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 67C24EB68B67E166AAF91AFC /* 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 = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A5460BC7374C6E7592B1FF3E /* 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 = ""; }; + C1FB72C1E9B25422AFEB740F /* 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 = ""; }; + C518FFB7BDFCCDF5D96D3B89 /* 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 = ""; }; + DBED630FEBB9A18F27C8E814 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AEA6F424A1A42303F9A597C0 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0C591566BA30BFA6D780386A /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 91771D8FA5A30CAEDA51E26A /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* veggieseasons.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 91771D8FA5A30CAEDA51E26A /* Pods */ = { + isa = PBXGroup; + children = ( + A5460BC7374C6E7592B1FF3E /* Pods-Runner.debug.xcconfig */, + C518FFB7BDFCCDF5D96D3B89 /* Pods-Runner.release.xcconfig */, + 4D8AF33CD5C878ED37254D67 /* Pods-Runner.profile.xcconfig */, + 413A4F2B0013AFEC349AB2C5 /* Pods-RunnerTests.debug.xcconfig */, + C1FB72C1E9B25422AFEB740F /* Pods-RunnerTests.release.xcconfig */, + 67C24EB68B67E166AAF91AFC /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4EE22A708C4E7F94406AFD15 /* Pods_Runner.framework */, + DBED630FEBB9A18F27C8E814 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + A9DCAE5A5FC2DAC764BF600F /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 6233C4FEDE0E579A6F92072D /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 6AA72EDC93A2C61F5DA982D4 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* veggieseasons.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 6233C4FEDE0E579A6F92072D /* [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; + }; + 6AA72EDC93A2C61F5DA982D4 /* [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; + }; + A9DCAE5A5FC2DAC764BF600F /* [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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 413A4F2B0013AFEC349AB2C5 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/veggieseasons.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/veggieseasons"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C1FB72C1E9B25422AFEB740F /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/veggieseasons.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/veggieseasons"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 67C24EB68B67E166AAF91AFC /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/veggieseasons.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/veggieseasons"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/veggieseasons/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/veggieseasons/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/veggieseasons/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/veggieseasons/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/veggieseasons/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..4c21638 --- /dev/null +++ b/veggieseasons/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/veggieseasons/macos/Runner.xcworkspace/contents.xcworkspacedata b/veggieseasons/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/veggieseasons/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/veggieseasons/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/veggieseasons/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/veggieseasons/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/veggieseasons/macos/Runner/AppDelegate.swift b/veggieseasons/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..8e02df2 --- /dev/null +++ b/veggieseasons/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/veggieseasons/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/veggieseasons/macos/Runner/Base.lproj/MainMenu.xib b/veggieseasons/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/veggieseasons/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/veggieseasons/macos/Runner/Configs/AppInfo.xcconfig b/veggieseasons/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..04ab23a --- /dev/null +++ b/veggieseasons/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = veggieseasons + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.veggieseasons + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2024 dev.flutter. All rights reserved. diff --git a/veggieseasons/macos/Runner/Configs/Debug.xcconfig b/veggieseasons/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/veggieseasons/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/veggieseasons/macos/Runner/Configs/Release.xcconfig b/veggieseasons/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/veggieseasons/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/veggieseasons/macos/Runner/Configs/Warnings.xcconfig b/veggieseasons/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/veggieseasons/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/veggieseasons/macos/Runner/DebugProfile.entitlements b/veggieseasons/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/veggieseasons/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/veggieseasons/macos/Runner/Info.plist b/veggieseasons/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/veggieseasons/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/veggieseasons/macos/Runner/MainFlutterWindow.swift b/veggieseasons/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/veggieseasons/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/veggieseasons/macos/Runner/Release.entitlements b/veggieseasons/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/veggieseasons/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/veggieseasons/macos/RunnerTests/RunnerTests.swift b/veggieseasons/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/veggieseasons/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/veggieseasons/pubspec.yaml b/veggieseasons/pubspec.yaml new file mode 100644 index 0000000..cd87de0 --- /dev/null +++ b/veggieseasons/pubspec.yaml @@ -0,0 +1,82 @@ +name: veggieseasons +description: An iOS app that shows the fruits and veggies currently in season. +publish_to: none +version: 1.2.0 +resolution: workspace + +environment: + sdk: ^3.9.0-0 + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + font_awesome_flutter: ^10.1.0 + intl: ^0.20.0 + provider: ^6.0.1 + shared_preferences: ^2.0.14 + window_size: + git: + url: https://github.com/google/flutter-desktop-embedding.git + path: plugins/window_size + # TODO: https://github.com/flutter/samples/issues/1838 + # go_router ^7.1.0 is breaking the state restoration tests + go_router: ^16.0.0 + +dev_dependencies: + analysis_defaults: + path: ../analysis_defaults + flutter_test: + sdk: flutter + flutter_launcher_icons: ^0.14.0 + +flutter: + assets: + - assets/images/apple.jpg + - assets/images/artichoke.jpg + - assets/images/asparagus.jpg + - assets/images/avocado.jpg + - assets/images/blackberry.jpg + - assets/images/cantaloupe.jpg + - assets/images/cauliflower.jpg + - assets/images/endive.jpg + - assets/images/fig.jpg + - assets/images/grape.jpg + - assets/images/green_bell_pepper.jpg + - assets/images/habanero.jpg + - assets/images/kale.jpg + - assets/images/kiwi.jpg + - assets/images/lemon.jpg + - assets/images/lime.jpg + - assets/images/mango.jpg + - assets/images/mushroom.jpg + - assets/images/nectarine.jpg + - assets/images/persimmon.jpg + - assets/images/plum.jpg + - assets/images/potato.jpg + - assets/images/radicchio.jpg + - assets/images/radish.jpg + - assets/images/squash.jpg + - assets/images/strawberry.jpg + - assets/images/tangelo.jpg + - assets/images/tomato.jpg + - assets/images/watermelon.jpg + - assets/images/orange_bell_pepper.jpg + + fonts: + - family: NotoSans + fonts: + - asset: assets/fonts/NotoSans-Regular.ttf + weight: 400 + - asset: assets/fonts/NotoSans-Bold.ttf + weight: 700 + - asset: assets/fonts/NotoSans-BoldItalic.ttf + weight: 700 + style: italic + - asset: assets/fonts/NotoSans-Italic.ttf + style: italic + weight: 400 + +flutter_icons: + ios: true + image_path: "assets/icon/launcher_icon.png" diff --git a/veggieseasons/test/widget_test.dart b/veggieseasons/test/widget_test.dart new file mode 100644 index 0000000..f1181b0 --- /dev/null +++ b/veggieseasons/test/widget_test.dart @@ -0,0 +1,11 @@ +// This is a basic Flutter widget test. +// To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter +// provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to +// find child widgets in the widget tree, read text, and verify that the values of widget properties +// are correct. + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('This test always passes', (tester) async {}); +} diff --git a/veggieseasons/web/icons/Icon-192.png b/veggieseasons/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/veggieseasons/web/icons/Icon-192.png differ diff --git a/veggieseasons/web/icons/Icon-512.png b/veggieseasons/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/veggieseasons/web/icons/Icon-512.png differ diff --git a/veggieseasons/web/index.html b/veggieseasons/web/index.html new file mode 100644 index 0000000..ee352d4 --- /dev/null +++ b/veggieseasons/web/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + veggieseasons + + + + + + diff --git a/veggieseasons/web/manifest.json b/veggieseasons/web/manifest.json new file mode 100644 index 0000000..c75ec99 --- /dev/null +++ b/veggieseasons/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "veggieseasons", + "short_name": "veggieseasons", + "start_url": ".", + "display": "minimal-ui", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.cc b/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index d141b74..0000000 --- a/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,17 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - FirebaseAuthPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); - FirebaseCorePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); -} diff --git a/web_dashboard/.gitignore b/web_dashboard/.gitignore new file mode 100644 index 0000000..a981f58 --- /dev/null +++ b/web_dashboard/.gitignore @@ -0,0 +1,34 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/web_dashboard/.metadata b/web_dashboard/.metadata new file mode 100644 index 0000000..4390eea --- /dev/null +++ b/web_dashboard/.metadata @@ -0,0 +1,10 @@ +# 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: bc6f270c584d1fdba81330090ef6e822b9082919 + channel: master + +project_type: app diff --git a/web_dashboard/README.md b/web_dashboard/README.md new file mode 100644 index 0000000..5daf683 --- /dev/null +++ b/web_dashboard/README.md @@ -0,0 +1,123 @@ +# web_dashboard + +**In progress** + +A dashboard app that displays daily entries. + +1. How to use an AdaptiveScaffold adaptive layout for large, medium, and small +screens. +2. How to use Firebase [Cloud +Firestore](https://firebase.google.com/docs/firestore) database with Google +Sign-In. +3. How to use [charts](https://pub.dev/packages/charts_flutter) to display +data. +4. (in progress) How to set up routing for a web app + +This app is web-first, and isn't guaranteed to run on iOS, Android or desktop +platforms. + +## Running + +Normal mode (DDC): + +``` +flutter run -d chrome +``` + +Skia / CanvasKit mode: + +``` +flutter run -d chrome --release --dart-define=FLUTTER_WEB_USE_SKIA=true +``` + +## Running JSON code generator + +``` +dart run grinder generate +``` + +## Add Firebase + +### Step 1: Create a new Firebase project + +Go to [console.firebase.google.com](https://console.firebase.google.com/) and +create a new Firebase project. + +### Step 2: Enable Google Sign In for your project + +In the Firebase console, go to "Authentication" and enable Google sign in. Click +on "Web SDK Configuration" and copy down your Web client ID. + +### Step 3: Add Client ID to `index.html` + +Uncomment this line in `index.html` and replace `` with the +client ID from Step 2: + +```html + + +``` + +### Step 4: Create a web app + +In the Firebase console, under "Project overview", click "Add app", select Web, +and replace the contents of `web/firebase_init.js`. + +```javascript +// web/firebase_init.js +var firebaseConfig = { + apiKey: "", + authDomain: "", + databaseURL: "", + projectId: "", + storageBucket: "", + messagingSenderId: "", + appId: "" +}; + +// Initialize Firebase +firebase.initializeApp(firebaseConfig); +``` + +### Step 4: Create Cloud Firestore + +Create a new Cloud Firestore database and add the following rules to disallow +users from reading/writing other users' data: + +``` +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + // Make sure the uid of the requesting user matches name of the user + // document. The wildcard expression {userId} makes the userId variable + // available in rules. + match /users/{userId}/{document=**} { + allow read, update, delete: if request.auth.uid == userId; + allow create: if request.auth.uid != null; + } + } +} +``` + +### Step 5: Run the app + +Run the app on port 5000: + +```bash +flutter run -d chrome --web-port=5000 +``` + +If you see CORS errors in your browser's console, go to the [Services +section][cloud-console-apis] in the Google Cloud console, go to Credentials, and +verify that `localhost:5000` is whitelisted. + +### (optional) Step 7: Set up iOS and Android +If you would like to run the app on iOS or Android, make sure you've installed +the appropriate configuration files described at +[firebase.google.com/docs/flutter/setup][flutter-setup] from step 1, and follow +the instructions detailed in the [google_sign_in README][google-sign-in] + +[flutter-setup]: https://firebase.google.com/docs/flutter/setup +[cloud-console-apis]: https://console.developers.google.com/apis/dashboard +[google-sign-in]: https://pub.dev/packages/google_sign_in diff --git a/web_dashboard/analysis_options.yaml b/web_dashboard/analysis_options.yaml new file mode 100644 index 0000000..13d6fe1 --- /dev/null +++ b/web_dashboard/analysis_options.yaml @@ -0,0 +1 @@ +include: package:analysis_defaults/flutter.yaml diff --git a/web_dashboard/lib/main.dart b/web_dashboard/lib/main.dart new file mode 100644 index 0000000..c0b1767 --- /dev/null +++ b/web_dashboard/lib/main.dart @@ -0,0 +1,11 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'src/app.dart'; + +void main() { + runApp(DashboardApp.mock()); +} diff --git a/web_dashboard/lib/main_firebase.dart b/web_dashboard/lib/main_firebase.dart new file mode 100644 index 0000000..20517fe --- /dev/null +++ b/web_dashboard/lib/main_firebase.dart @@ -0,0 +1,11 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'src/app.dart'; + +void main() { + runApp(DashboardApp.firebase()); +} diff --git a/web_dashboard/lib/src/api/api.dart b/web_dashboard/lib/src/api/api.dart new file mode 100644 index 0000000..0ab9dfa --- /dev/null +++ b/web_dashboard/lib/src/api/api.dart @@ -0,0 +1,109 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'api.g.dart'; + +/// Manipulates app data, +abstract class DashboardApi { + CategoryApi get categories; + EntryApi get entries; +} + +/// Manipulates [Category] data. +abstract class CategoryApi { + Future delete(String id); + + Future get(String id); + + Future insert(Category category); + + Future> list(); + + Future update(Category category, String id); + + Stream> subscribe(); +} + +/// Manipulates [Entry] data. +abstract class EntryApi { + Future delete(String categoryId, String id); + + Future get(String categoryId, String id); + + Future insert(String categoryId, Entry entry); + + Future> list(String categoryId); + + Future update(String categoryId, String id, Entry entry); + + Stream> subscribe(String categoryId); +} + +/// Something that's being tracked, e.g. Hours Slept, Cups of water, etc. +@JsonSerializable() +class Category { + String name; + + @JsonKey(includeFromJson: false) + String? id; + + Category(this.name); + + factory Category.fromJson(Map json) => + _$CategoryFromJson(json); + + Map toJson() => _$CategoryToJson(this); + + @override + operator ==(Object other) => other is Category && other.id == id; + @override + int get hashCode => id.hashCode; + @override + String toString() { + return ''; + } +} + +/// A number tracked at a point in time. +@JsonSerializable() +class Entry { + int value; + @JsonKey(fromJson: _timestampToDateTime, toJson: _dateTimeToTimestamp) + DateTime time; + + @JsonKey(includeFromJson: false) + String? id; + + Entry(this.value, this.time); + + factory Entry.fromJson(Map json) => _$EntryFromJson(json); + + Map toJson() => _$EntryToJson(this); + + static DateTime _timestampToDateTime(Timestamp timestamp) { + return DateTime.fromMillisecondsSinceEpoch( + timestamp.millisecondsSinceEpoch, + ); + } + + static Timestamp _dateTimeToTimestamp(DateTime dateTime) { + return Timestamp.fromMillisecondsSinceEpoch( + dateTime.millisecondsSinceEpoch, + ); + } + + @override + operator ==(Object other) => other is Entry && other.id == id; + + @override + int get hashCode => id.hashCode; + + @override + String toString() { + return ''; + } +} diff --git a/web_dashboard/lib/src/api/api.g.dart b/web_dashboard/lib/src/api/api.g.dart new file mode 100644 index 0000000..6a90026 --- /dev/null +++ b/web_dashboard/lib/src/api/api.g.dart @@ -0,0 +1,31 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Category _$CategoryFromJson(Map json) { + return Category(json['name'] as String); +} + +Map _$CategoryToJson(Category instance) => { + 'name': instance.name, +}; + +Entry _$EntryFromJson(Map json) { + return Entry( + json['value'] as int, + Entry._timestampToDateTime(json['time'] as Timestamp), + ); +} + +Map _$EntryToJson(Entry instance) => { + 'value': instance.value, + 'time': Entry._dateTimeToTimestamp(instance.time), +}; diff --git a/web_dashboard/lib/src/api/firebase.dart b/web_dashboard/lib/src/api/firebase.dart new file mode 100644 index 0000000..6ba42e8 --- /dev/null +++ b/web_dashboard/lib/src/api/firebase.dart @@ -0,0 +1,150 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart'; + +import 'api.dart'; + +class FirebaseDashboardApi implements DashboardApi { + @override + final EntryApi entries; + + @override + final CategoryApi categories; + + FirebaseDashboardApi(FirebaseFirestore firestore, String userId) + : entries = FirebaseEntryApi(firestore, userId), + categories = FirebaseCategoryApi(firestore, userId); +} + +class FirebaseEntryApi implements EntryApi { + final FirebaseFirestore firestore; + final String userId; + final CollectionReference> _categoriesRef; + + FirebaseEntryApi(this.firestore, this.userId) + : _categoriesRef = firestore.collection('users/$userId/categories'); + + @override + Stream> subscribe(String categoryId) { + var snapshots = + _categoriesRef.doc(categoryId).collection('entries').snapshots(); + var result = snapshots.map>((querySnapshot) { + return querySnapshot.docs.map((snapshot) { + return Entry.fromJson(snapshot.data())..id = snapshot.id; + }).toList(); + }); + + return result; + } + + @override + Future delete(String categoryId, String id) async { + var document = _categoriesRef.doc('$categoryId/entries/$id'); + var entry = await get(categoryId, document.id); + + await document.delete(); + + return entry; + } + + @override + Future insert(String categoryId, Entry entry) async { + var document = await _categoriesRef + .doc(categoryId) + .collection('entries') + .add(entry.toJson()); + return await get(categoryId, document.id); + } + + @override + Future> list(String categoryId) async { + var entriesRef = _categoriesRef.doc(categoryId).collection('entries'); + var querySnapshot = await entriesRef.get(); + var entries = + querySnapshot.docs + .map((doc) => Entry.fromJson(doc.data())..id = doc.id) + .toList(); + + return entries; + } + + @override + Future update(String categoryId, String id, Entry entry) async { + var document = _categoriesRef.doc('$categoryId/entries/$id'); + await document.update(entry.toJson()); + var snapshot = await document.get(); + return Entry.fromJson(snapshot.data()!)..id = snapshot.id; + } + + @override + Future get(String categoryId, String id) async { + var document = _categoriesRef.doc('$categoryId/entries/$id'); + var snapshot = await document.get(); + return Entry.fromJson(snapshot.data()!)..id = snapshot.id; + } +} + +class FirebaseCategoryApi implements CategoryApi { + final FirebaseFirestore firestore; + final String userId; + final CollectionReference> _categoriesRef; + + FirebaseCategoryApi(this.firestore, this.userId) + : _categoriesRef = firestore.collection('users/$userId/categories'); + + @override + Stream> subscribe() { + var snapshots = _categoriesRef.snapshots(); + var result = snapshots.map>((querySnapshot) { + return querySnapshot.docs.map((snapshot) { + return Category.fromJson(snapshot.data())..id = snapshot.id; + }).toList(); + }); + + return result; + } + + @override + Future delete(String id) async { + var document = _categoriesRef.doc(id); + var categories = await get(document.id); + + await document.delete(); + + return categories; + } + + @override + Future get(String id) async { + var document = _categoriesRef.doc(id); + var snapshot = await document.get(); + return Category.fromJson(snapshot.data()!)..id = snapshot.id; + } + + @override + Future insert(Category category) async { + var document = await _categoriesRef.add(category.toJson()); + return await get(document.id); + } + + @override + Future> list() async { + var querySnapshot = await _categoriesRef.get(); + var categories = + querySnapshot.docs + .map((doc) => Category.fromJson(doc.data())..id = doc.id) + .toList(); + + return categories; + } + + @override + Future update(Category category, String id) async { + var document = _categoriesRef.doc(id); + await document.update(category.toJson()); + var snapshot = await document.get(); + return Category.fromJson(snapshot.data()!)..id = snapshot.id; + } +} diff --git a/web_dashboard/lib/src/api/mock.dart b/web_dashboard/lib/src/api/mock.dart new file mode 100644 index 0000000..463d8b2 --- /dev/null +++ b/web_dashboard/lib/src/api/mock.dart @@ -0,0 +1,151 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:uuid/uuid.dart' as uuid; + +import 'api.dart'; + +class MockDashboardApi implements DashboardApi { + @override + final EntryApi entries = MockEntryApi(); + + @override + final CategoryApi categories = MockCategoryApi(); + + MockDashboardApi(); + + /// Creates a [MockDashboardApi] filled with mock data for the last 30 days. + Future fillWithMockData() async { + await Future.delayed(const Duration(seconds: 1)); + var category1 = await categories.insert(Category('Coffee (oz)')); + var category2 = await categories.insert(Category('Running (miles)')); + var category3 = await categories.insert(Category('Git Commits')); + var monthAgo = DateTime.now().subtract(const Duration(days: 30)); + + for (var category in [category1, category2, category3]) { + for (var i = 0; i < 30; i++) { + var date = monthAgo.add(Duration(days: i)); + var value = Random().nextInt(6) + 1; + await entries.insert(category.id!, Entry(value, date)); + } + } + } +} + +class MockCategoryApi implements CategoryApi { + final Map _storage = {}; + final StreamController> _streamController = + StreamController>.broadcast(); + + @override + Future delete(String id) async { + var removed = _storage.remove(id); + _emit(); + return removed; + } + + @override + Future get(String id) async { + return _storage[id]; + } + + @override + Future insert(Category category) async { + var id = const uuid.Uuid().v4(); + var newCategory = Category(category.name)..id = id; + _storage[id] = newCategory; + _emit(); + return newCategory; + } + + @override + Future> list() async { + return _storage.values.toList(); + } + + @override + Future update(Category category, String id) async { + _storage[id] = category; + _emit(); + return category..id = id; + } + + @override + Stream> subscribe() => _streamController.stream; + + void _emit() { + _streamController.add(_storage.values.toList()); + } +} + +class MockEntryApi implements EntryApi { + final Map _storage = {}; + final StreamController<_EntriesEvent> _streamController = + StreamController.broadcast(); + + @override + Future delete(String categoryId, String id) async { + _emit(categoryId); + return _storage.remove('$categoryId-$id'); + } + + @override + Future insert(String categoryId, Entry entry) async { + var id = const uuid.Uuid().v4(); + var newEntry = Entry(entry.value, entry.time)..id = id; + _storage['$categoryId-$id'] = newEntry; + _emit(categoryId); + return newEntry; + } + + @override + Future> list(String categoryId) async { + var list = + _storage.keys + .where((k) => k.startsWith(categoryId)) + .map((k) => _storage[k]) + .nonNulls + .toList(); + return list; + } + + @override + Future update(String categoryId, String id, Entry entry) async { + _storage['$categoryId-$id'] = entry; + _emit(categoryId); + return entry..id = id; + } + + @override + Stream> subscribe(String categoryId) { + return _streamController.stream + .where((event) => event.categoryId == categoryId) + .map((event) => event.entries); + } + + void _emit(String categoryId) { + var entries = + _storage.keys + .where((k) => k.startsWith(categoryId)) + .map((k) => _storage[k]!) + .toList(); + + _streamController.add(_EntriesEvent(categoryId, entries)); + } + + @override + Future get(String categoryId, String id) async { + return _storage['$categoryId-$id']; + } +} + +class _EntriesEvent { + final String categoryId; + final List entries; + + _EntriesEvent(this.categoryId, this.entries); +} diff --git a/web_dashboard/lib/src/app.dart b/web_dashboard/lib/src/app.dart new file mode 100644 index 0000000..44c247d --- /dev/null +++ b/web_dashboard/lib/src/app.dart @@ -0,0 +1,124 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'api/api.dart'; +import 'api/firebase.dart'; +import 'api/mock.dart'; +import 'auth/auth.dart'; +import 'auth/firebase.dart'; +import 'auth/mock.dart'; +import 'pages/home.dart'; +import 'pages/sign_in.dart'; + +/// The global state the app. +class AppState { + final Auth auth; + DashboardApi? api; + + AppState(this.auth); +} + +/// Creates a [DashboardApi] for the given user. This allows users of this +/// widget to specify whether [MockDashboardApi] or [ApiBuilder] should be +/// created when the user logs in. +typedef ApiBuilder = DashboardApi Function(User user); + +/// An app that displays a personalized dashboard. +class DashboardApp extends StatefulWidget { + static DashboardApi _mockApiBuilder(User user) => + MockDashboardApi()..fillWithMockData(); + static DashboardApi _apiBuilder(User user) => + FirebaseDashboardApi(FirebaseFirestore.instance, user.uid); + + final Auth auth; + final ApiBuilder apiBuilder; + + /// Runs the app using Firebase + DashboardApp.firebase({super.key}) + : auth = FirebaseAuthService(), + apiBuilder = _apiBuilder; + + /// Runs the app using mock data + DashboardApp.mock({super.key}) + : auth = MockAuthService(), + apiBuilder = _mockApiBuilder; + + @override + State createState() => _DashboardAppState(); +} + +class _DashboardAppState extends State { + late final AppState _appState; + + @override + void initState() { + super.initState(); + _appState = AppState(widget.auth); + } + + @override + Widget build(BuildContext context) { + return Provider.value( + value: _appState, + child: MaterialApp( + theme: ThemeData.light(), + home: SignInSwitcher( + appState: _appState, + apiBuilder: widget.apiBuilder, + ), + ), + ); + } +} + +/// Switches between showing the [SignInPage] or [HomePage], depending on +/// whether or not the user is signed in. +class SignInSwitcher extends StatefulWidget { + final AppState? appState; + final ApiBuilder? apiBuilder; + + const SignInSwitcher({this.appState, this.apiBuilder, super.key}); + + @override + State createState() => _SignInSwitcherState(); +} + +class _SignInSwitcherState extends State { + bool _isSignedIn = false; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + child: + _isSignedIn + ? HomePage(onSignOut: _handleSignOut) + : SignInPage( + auth: widget.appState!.auth, + onSuccess: _handleSignIn, + ), + ); + } + + void _handleSignIn(User user) { + widget.appState!.api = widget.apiBuilder!(user); + + setState(() { + _isSignedIn = true; + }); + } + + Future _handleSignOut() async { + await widget.appState!.auth.signOut(); + setState(() { + _isSignedIn = false; + }); + } +} diff --git a/web_dashboard/lib/src/auth/auth.dart b/web_dashboard/lib/src/auth/auth.dart new file mode 100644 index 0000000..24cb700 --- /dev/null +++ b/web_dashboard/lib/src/auth/auth.dart @@ -0,0 +1,15 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +abstract class Auth { + Future get isSignedIn; + Future signIn(); + Future signOut(); +} + +abstract class User { + String get uid; +} + +class SignInException implements Exception {} diff --git a/web_dashboard/lib/src/auth/firebase.dart b/web_dashboard/lib/src/auth/firebase.dart new file mode 100644 index 0000000..81a798a --- /dev/null +++ b/web_dashboard/lib/src/auth/firebase.dart @@ -0,0 +1,58 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_auth/firebase_auth.dart' hide User; +import 'package:flutter/services.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +import 'auth.dart'; + +class FirebaseAuthService implements Auth { + final GoogleSignIn _googleSignIn = GoogleSignIn(); + final FirebaseAuth _auth = FirebaseAuth.instance; + + @override + Future get isSignedIn => _googleSignIn.isSignedIn(); + + @override + Future signIn() async { + try { + return await _signIn(); + } on PlatformException { + throw SignInException(); + } + } + + Future _signIn() async { + GoogleSignInAccount? googleUser; + if (await isSignedIn) { + googleUser = await _googleSignIn.signInSilently(); + } else { + googleUser = await _googleSignIn.signIn(); + } + + var googleAuth = await googleUser!.authentication; + + var credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + var authResult = await _auth.signInWithCredential(credential); + + return _FirebaseUser(authResult.user!.uid); + } + + @override + Future signOut() async { + await Future.wait([_auth.signOut(), _googleSignIn.signOut()]); + } +} + +class _FirebaseUser implements User { + @override + final String uid; + + _FirebaseUser(this.uid); +} diff --git a/web_dashboard/lib/src/auth/mock.dart b/web_dashboard/lib/src/auth/mock.dart new file mode 100644 index 0000000..e54d518 --- /dev/null +++ b/web_dashboard/lib/src/auth/mock.dart @@ -0,0 +1,25 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'auth.dart'; + +class MockAuthService implements Auth { + @override + Future get isSignedIn async => false; + + @override + Future signIn() async { + return MockUser(); + } + + @override + Future signOut() async { + return null; + } +} + +class MockUser implements User { + @override + String get uid => "123"; +} diff --git a/web_dashboard/lib/src/pages/dashboard.dart b/web_dashboard/lib/src/pages/dashboard.dart new file mode 100644 index 0000000..d120b11 --- /dev/null +++ b/web_dashboard/lib/src/pages/dashboard.dart @@ -0,0 +1,62 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../api/api.dart'; +import '../app.dart'; +import '../widgets/category_chart.dart'; + +class DashboardPage extends StatelessWidget { + const DashboardPage({super.key}); + + @override + Widget build(BuildContext context) { + var appState = Provider.of(context); + return FutureBuilder>( + future: appState.api!.categories.list(), + builder: (context, futureSnapshot) { + if (!futureSnapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + return StreamBuilder>( + initialData: futureSnapshot.data, + stream: appState.api!.categories.subscribe(), + builder: (context, snapshot) { + if (snapshot.data == null) { + return const Center(child: CircularProgressIndicator()); + } + return Dashboard(snapshot.data); + }, + ); + }, + ); + } +} + +class Dashboard extends StatelessWidget { + final List? categories; + + const Dashboard(this.categories, {super.key}); + + @override + Widget build(BuildContext context) { + var api = Provider.of(context).api; + return Scrollbar( + child: GridView( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + childAspectRatio: 2, + maxCrossAxisExtent: 500, + ), + children: [ + ...categories!.map( + (category) => + Card(child: CategoryChart(category: category, api: api)), + ), + ], + ), + ); + } +} diff --git a/web_dashboard/lib/src/pages/entries.dart b/web_dashboard/lib/src/pages/entries.dart new file mode 100644 index 0000000..0220c97 --- /dev/null +++ b/web_dashboard/lib/src/pages/entries.dart @@ -0,0 +1,157 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' as intl; +import 'package:provider/provider.dart'; + +import '../api/api.dart'; +import '../app.dart'; +import '../widgets/categories_dropdown.dart'; +import '../widgets/dialogs.dart'; + +class EntriesPage extends StatefulWidget { + const EntriesPage({super.key}); + + @override + State createState() => _EntriesPageState(); +} + +class _EntriesPageState extends State { + Category? _selected; + + @override + Widget build(BuildContext context) { + var appState = Provider.of(context); + return Column( + children: [ + CategoryDropdown( + api: appState.api!.categories, + onSelected: (category) => setState(() => _selected = category), + ), + Expanded( + child: + _selected == null + ? const Center(child: CircularProgressIndicator()) + : EntriesList( + category: _selected, + api: appState.api!.entries, + ), + ), + ], + ); + } +} + +class EntriesList extends StatefulWidget { + final Category? category; + final EntryApi api; + + EntriesList({this.category, required this.api}) + : super(key: ValueKey(category?.id)); + + @override + State createState() => _EntriesListState(); +} + +class _EntriesListState extends State { + @override + Widget build(BuildContext context) { + if (widget.category == null) { + return _buildLoadingIndicator(); + } + + return FutureBuilder>( + future: widget.api.list(widget.category!.id!), + builder: (context, futureSnapshot) { + if (!futureSnapshot.hasData) { + return _buildLoadingIndicator(); + } + return StreamBuilder>( + initialData: futureSnapshot.data, + stream: widget.api.subscribe(widget.category!.id!), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return _buildLoadingIndicator(); + } + return ListView.builder( + itemBuilder: (context, index) { + return EntryTile( + category: widget.category, + entry: snapshot.data![index], + ); + }, + itemCount: snapshot.data!.length, + ); + }, + ); + }, + ); + } + + Widget _buildLoadingIndicator() { + return const Center(child: CircularProgressIndicator()); + } +} + +class EntryTile extends StatelessWidget { + final Category? category; + final Entry? entry; + + const EntryTile({this.category, this.entry, super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(entry!.value.toString()), + subtitle: Text(intl.DateFormat('MM/dd/yy h:mm a').format(entry!.time)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + child: const Text('Edit'), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return EditEntryDialog(category: category, entry: entry); + }, + ); + }, + ), + TextButton( + child: const Text('Delete'), + onPressed: () async { + final appState = Provider.of(context, listen: false); + final scaffoldMessenger = ScaffoldMessenger.of(context); + final bool? shouldDelete = await showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('Delete entry?'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(false), + ), + TextButton( + child: const Text('Delete'), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ), + ); + if (shouldDelete != null && shouldDelete) { + await appState.api!.entries.delete(category!.id!, entry!.id!); + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Entry deleted')), + ); + } + }, + ), + ], + ), + ); + } +} diff --git a/web_dashboard/lib/src/pages/home.dart b/web_dashboard/lib/src/pages/home.dart new file mode 100644 index 0000000..9e9b652 --- /dev/null +++ b/web_dashboard/lib/src/pages/home.dart @@ -0,0 +1,128 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../widgets/dialogs.dart'; +import '../widgets/third_party/adaptive_scaffold.dart'; +import 'dashboard.dart'; +import 'entries.dart'; + +class HomePage extends StatefulWidget { + final VoidCallback onSignOut; + + const HomePage({required this.onSignOut, super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + int _pageIndex = 0; + + @override + Widget build(BuildContext context) { + return AdaptiveScaffold( + title: const Text('Dashboard App'), + actions: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextButton( + style: TextButton.styleFrom(foregroundColor: Colors.white), + onPressed: () => _handleSignOut(), + child: const Text('Sign Out'), + ), + ), + ], + currentIndex: _pageIndex, + destinations: const [ + AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home), + AdaptiveScaffoldDestination(title: 'Entries', icon: Icons.list), + AdaptiveScaffoldDestination(title: 'Settings', icon: Icons.settings), + ], + body: _pageAtIndex(_pageIndex), + onNavigationIndexChange: (newIndex) { + setState(() { + _pageIndex = newIndex; + }); + }, + floatingActionButton: + _hasFloatingActionButton ? _buildFab(context) : null, + ); + } + + bool get _hasFloatingActionButton { + if (_pageIndex == 2) return false; + return true; + } + + FloatingActionButton _buildFab(BuildContext context) { + return FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () => _handleFabPressed(), + ); + } + + void _handleFabPressed() { + if (_pageIndex == 0) { + showDialog( + context: context, + builder: (context) => const NewCategoryDialog(), + ); + return; + } + + if (_pageIndex == 1) { + showDialog( + context: context, + builder: (context) => const NewEntryDialog(), + ); + return; + } + } + + Future _handleSignOut() async { + var shouldSignOut = await (showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('Are you sure you want to sign out?'), + actions: [ + TextButton( + child: const Text('No'), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + TextButton( + child: const Text('Yes'), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ), + )); + + if (shouldSignOut == null || !shouldSignOut) { + return; + } + + widget.onSignOut(); + } + + static Widget _pageAtIndex(int index) { + if (index == 0) { + return const DashboardPage(); + } + + if (index == 1) { + return const EntriesPage(); + } + + return const Center(child: Text('Settings page')); + } +} diff --git a/web_dashboard/lib/src/pages/sign_in.dart b/web_dashboard/lib/src/pages/sign_in.dart new file mode 100644 index 0000000..d441be5 --- /dev/null +++ b/web_dashboard/lib/src/pages/sign_in.dart @@ -0,0 +1,94 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../auth/auth.dart'; + +class SignInPage extends StatelessWidget { + final Auth auth; + final ValueChanged onSuccess; + + const SignInPage({required this.auth, required this.onSuccess, super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center(child: SignInButton(auth: auth, onSuccess: onSuccess)), + ); + } +} + +class SignInButton extends StatefulWidget { + final Auth auth; + final ValueChanged onSuccess; + + const SignInButton({required this.auth, required this.onSuccess, super.key}); + + @override + State createState() => _SignInButtonState(); +} + +class _SignInButtonState extends State { + Future? _checkSignInFuture; + + @override + void initState() { + super.initState(); + _checkSignInFuture = _checkIfSignedIn(); + } + + // Check if the user is signed in. If the user is already signed in (for + // example, if they signed in and refreshed the page), invoke the `onSuccess` + // callback right away. + Future _checkIfSignedIn() async { + var alreadySignedIn = await widget.auth.isSignedIn; + if (alreadySignedIn) { + var user = await widget.auth.signIn(); + widget.onSuccess(user); + } + return alreadySignedIn; + } + + Future _signIn() async { + try { + var user = await widget.auth.signIn(); + widget.onSuccess(user); + } on SignInException { + _showError(); + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _checkSignInFuture, + builder: (context, snapshot) { + // If signed in, or the future is incomplete, show a circular + // progress indicator. + var alreadySignedIn = snapshot.data; + if (snapshot.connectionState != ConnectionState.done || + alreadySignedIn == true) { + return const CircularProgressIndicator(); + } + + // If sign in failed, show toast and the login button + if (snapshot.hasError) { + _showError(); + } + + return FilledButton( + child: const Text('Sign In with Google'), + onPressed: () => _signIn(), + ); + }, + ); + } + + void _showError() { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Unable to sign in.'))); + } +} diff --git a/web_dashboard/lib/src/utils/chart_utils.dart b/web_dashboard/lib/src/utils/chart_utils.dart new file mode 100644 index 0000000..bbb2384 --- /dev/null +++ b/web_dashboard/lib/src/utils/chart_utils.dart @@ -0,0 +1,76 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../api/api.dart'; +import 'day_helpers.dart'; + +/// The total value of one or more [Entry]s on a given day. +class EntryTotal { + final DateTime day; + int value; + + EntryTotal(this.day, this.value); +} + +/// Returns a list of [EntryTotal] objects. Each [EntryTotal] is the sum of +/// the values of all the entries on a given day. +List entryTotalsByDay( + List? entries, + int daysAgo, { + DateTime? today, +}) { + today ??= DateTime.now(); + return _entryTotalsByDay(entries, daysAgo, today).toList(); +} + +Iterable _entryTotalsByDay( + List? entries, + int daysAgo, + DateTime today, +) sync* { + var start = today.subtract(Duration(days: daysAgo)); + var entriesByDay = _entriesInRange(start, today, entries); + + for (var i = 0; i < entriesByDay.length; i++) { + var list = entriesByDay[i]; + var entryTotal = EntryTotal(start.add(Duration(days: i)), 0); + + for (var entry in list) { + entryTotal.value += entry.value; + } + + yield entryTotal; + } +} + +/// Groups entries by day between [start] and [end]. The result is a list of +/// lists. The outer list represents the number of days since [start], and the +/// inner list is the group of entries on that day. +List> _entriesInRange( + DateTime start, + DateTime end, + List? entries, +) => _entriesInRangeImpl(start, end, entries).toList(); + +Iterable> _entriesInRangeImpl( + DateTime start, + DateTime end, + List? entries, +) sync* { + start = start.atMidnight; + end = end.atMidnight; + var d = start; + + while (d.compareTo(end) <= 0) { + var es = []; + for (var entry in entries!) { + if (d.isSameDay(entry.time.atMidnight)) { + es.add(entry); + } + } + + yield es; + d = d.add(const Duration(days: 1)); + } +} diff --git a/web_dashboard/lib/src/utils/day_helpers.dart b/web_dashboard/lib/src/utils/day_helpers.dart new file mode 100644 index 0000000..9b89938 --- /dev/null +++ b/web_dashboard/lib/src/utils/day_helpers.dart @@ -0,0 +1,15 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +extension DayUtils on DateTime { + /// The UTC date portion of a datetime, without the minutes, seconds, etc. + DateTime get atMidnight { + return DateTime.utc(year, month, day); + } + + /// Checks that the two [DateTime]s share the same date. + bool isSameDay(DateTime d2) { + return year == d2.year && month == d2.month && day == d2.day; + } +} diff --git a/web_dashboard/lib/src/widgets/categories_dropdown.dart b/web_dashboard/lib/src/widgets/categories_dropdown.dart new file mode 100644 index 0000000..3cfc6bf --- /dev/null +++ b/web_dashboard/lib/src/widgets/categories_dropdown.dart @@ -0,0 +1,112 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import '../api/api.dart'; + +/// Subscribes to the latest list of categories and allows the user to select +/// one. +class CategoryDropdown extends StatefulWidget { + final CategoryApi api; + final ValueChanged onSelected; + + const CategoryDropdown({ + required this.api, + required this.onSelected, + super.key, + }); + + @override + State createState() => _CategoryDropdownState(); +} + +class _CategoryDropdownState extends State { + Category? _selected; + Future>? _future; + Stream>? _stream; + + @override + void initState() { + super.initState(); + + // This widget needs to wait for the list of Categories, select the first + // Category, and emit an `onSelected` event. + // + // This could be done inside the FutureBuilder's `builder` callback, + // but calling setState() during the build is an error. (Calling the + // onSelected callback will also cause the parent widget to call + // setState()). + // + // Instead, we'll create a new Future that sets the selected Category and + // calls `onSelected` if necessary. Then, we'll pass *that* future to + // FutureBuilder. Now the selected category is set and events are emitted + // *before* the build is triggered by the FutureBuilder. + _future = widget.api.list().then((categories) { + if (categories.isEmpty) { + return categories; + } + + _setSelected(categories.first); + return categories; + }); + + // Same here, we'll create a new stream that handles any potential + // setState() operations before we trigger our StreamBuilder. + _stream = widget.api.subscribe().map((categories) { + if (!categories.contains(_selected) && categories.isNotEmpty) { + _setSelected(categories.first); + } + + return categories; + }); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _future, + builder: (context, futureSnapshot) { + // Show an empty dropdown while the data is loading. + if (!futureSnapshot.hasData) { + return DropdownButton(items: const [], onChanged: null); + } + + return StreamBuilder>( + initialData: futureSnapshot.hasData ? futureSnapshot.data : [], + stream: _stream, + builder: (context, snapshot) { + var data = snapshot.hasData ? snapshot.data! : []; + return DropdownButton( + value: _selected, + items: data.map(_buildDropdownItem).toList(), + onChanged: (category) { + _setSelected(category); + }, + ); + }, + ); + }, + ); + } + + void _setSelected(Category? category) { + if (_selected == category) { + return; + } + setState(() { + _selected = category; + }); + + widget.onSelected(_selected); + } + + DropdownMenuItem _buildDropdownItem(Category category) { + return DropdownMenuItem( + value: category, + child: Text(category.name), + ); + } +} diff --git a/web_dashboard/lib/src/widgets/category_chart.dart b/web_dashboard/lib/src/widgets/category_chart.dart new file mode 100644 index 0000000..48fba2e --- /dev/null +++ b/web_dashboard/lib/src/widgets/category_chart.dart @@ -0,0 +1,102 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:community_charts_flutter/community_charts_flutter.dart' + as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' as intl; + +import '../api/api.dart'; +import '../utils/chart_utils.dart' as utils; +import 'dialogs.dart'; + +// The number of days to show in the chart +const _daysBefore = 10; + +class CategoryChart extends StatelessWidget { + final Category category; + final DashboardApi? api; + + const CategoryChart({required this.category, required this.api, super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(category.name), + IconButton( + icon: const Icon(Icons.settings), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return EditCategoryDialog(category: category); + }, + ); + }, + ), + ], + ), + ), + Expanded( + // Load the initial snapshot using a FutureBuilder, and subscribe to + // additional updates with a StreamBuilder. + child: FutureBuilder>( + future: api!.entries.list(category.id!), + builder: (context, futureSnapshot) { + if (!futureSnapshot.hasData) { + return _buildLoadingIndicator(); + } + return StreamBuilder>( + initialData: futureSnapshot.data, + stream: api!.entries.subscribe(category.id!), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return _buildLoadingIndicator(); + } + return _BarChart(entries: snapshot.data); + }, + ); + }, + ), + ), + ], + ); + } + + Widget _buildLoadingIndicator() { + return const Center(child: CircularProgressIndicator()); + } +} + +class _BarChart extends StatelessWidget { + final List? entries; + + const _BarChart({this.entries}); + + @override + Widget build(BuildContext context) { + return charts.BarChart([_seriesData()], animate: false); + } + + charts.Series _seriesData() { + return charts.Series( + id: 'Entries', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (entryTotal, _) { + var format = intl.DateFormat.Md(); + return format.format(entryTotal.day); + }, + measureFn: (total, _) { + return total.value; + }, + data: utils.entryTotalsByDay(entries, _daysBefore), + ); + } +} diff --git a/web_dashboard/lib/src/widgets/category_forms.dart b/web_dashboard/lib/src/widgets/category_forms.dart new file mode 100644 index 0000000..e60ce29 --- /dev/null +++ b/web_dashboard/lib/src/widgets/category_forms.dart @@ -0,0 +1,104 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:web_dashboard/src/api/api.dart'; +import 'package:web_dashboard/src/app.dart'; + +class NewCategoryForm extends StatefulWidget { + const NewCategoryForm({super.key}); + + @override + State createState() => _NewCategoryFormState(); +} + +class _NewCategoryFormState extends State { + final Category _category = Category(''); + + @override + Widget build(BuildContext context) { + var api = Provider.of(context).api; + return EditCategoryForm( + category: _category, + onDone: (shouldInsert) { + if (shouldInsert) { + api!.categories.insert(_category); + } + Navigator.of(context).pop(); + }, + ); + } +} + +class EditCategoryForm extends StatefulWidget { + final Category category; + final ValueChanged onDone; + + const EditCategoryForm({ + required this.category, + required this.onDone, + super.key, + }); + + @override + State createState() => _EditCategoryFormState(); +} + +class _EditCategoryFormState extends State { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + initialValue: widget.category.name, + decoration: const InputDecoration(labelText: 'Name'), + onChanged: (newValue) { + widget.category.name = newValue; + }, + validator: (value) { + if (value!.isEmpty) { + return 'Please enter a name'; + } + return null; + }, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: FilledButton( + child: const Text('Cancel'), + onPressed: () { + widget.onDone(false); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: FilledButton( + child: const Text('OK'), + onPressed: () { + if (_formKey.currentState!.validate()) { + widget.onDone(true); + } + }, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/web_dashboard/lib/src/widgets/dialogs.dart b/web_dashboard/lib/src/widgets/dialogs.dart new file mode 100644 index 0000000..91ae49b --- /dev/null +++ b/web_dashboard/lib/src/widgets/dialogs.dart @@ -0,0 +1,93 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:web_dashboard/src/api/api.dart'; +import 'package:web_dashboard/src/widgets/category_forms.dart'; + +import '../app.dart'; +import 'edit_entry.dart'; + +class NewCategoryDialog extends StatelessWidget { + const NewCategoryDialog({super.key}); + + @override + Widget build(BuildContext context) { + return const SimpleDialog( + title: Text('New Category'), + children: [NewCategoryForm()], + ); + } +} + +class EditCategoryDialog extends StatelessWidget { + final Category category; + + const EditCategoryDialog({required this.category, super.key}); + + @override + Widget build(BuildContext context) { + var api = Provider.of(context).api; + + return SimpleDialog( + title: const Text('Edit Category'), + children: [ + EditCategoryForm( + category: category, + onDone: (shouldUpdate) { + if (shouldUpdate) { + api!.categories.update(category, category.id!); + } + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} + +class NewEntryDialog extends StatefulWidget { + const NewEntryDialog({super.key}); + + @override + State createState() => _NewEntryDialogState(); +} + +class _NewEntryDialogState extends State { + @override + Widget build(BuildContext context) { + return const SimpleDialog( + title: Text('New Entry'), + children: [NewEntryForm()], + ); + } +} + +class EditEntryDialog extends StatelessWidget { + final Category? category; + final Entry? entry; + + const EditEntryDialog({this.category, this.entry, super.key}); + + @override + Widget build(BuildContext context) { + var api = Provider.of(context).api; + + return SimpleDialog( + title: const Text('Edit Entry'), + children: [ + EditEntryForm( + entry: entry, + onDone: (shouldUpdate) { + if (shouldUpdate) { + api!.entries.update(category!.id!, entry!.id!, entry!); + } + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/web_dashboard/lib/src/widgets/edit_entry.dart b/web_dashboard/lib/src/widgets/edit_entry.dart new file mode 100644 index 0000000..d08aac2 --- /dev/null +++ b/web_dashboard/lib/src/widgets/edit_entry.dart @@ -0,0 +1,153 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' as intl; +import 'package:provider/provider.dart'; +import 'package:web_dashboard/src/api/api.dart'; + +import '../app.dart'; +import 'categories_dropdown.dart'; + +class NewEntryForm extends StatefulWidget { + const NewEntryForm({super.key}); + + @override + State createState() => _NewEntryFormState(); +} + +class _NewEntryFormState extends State { + late Category _selected; + final Entry _entry = Entry(0, DateTime.now()); + + @override + Widget build(BuildContext context) { + var api = Provider.of(context).api!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: CategoryDropdown( + api: api.categories, + onSelected: (category) { + if (category == null) return; + setState(() { + _selected = category; + }); + }, + ), + ), + EditEntryForm( + entry: _entry, + onDone: (shouldInsert) { + if (shouldInsert) { + api.entries.insert(_selected.id!, _entry); + } + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} + +class EditEntryForm extends StatefulWidget { + final Entry? entry; + final ValueChanged onDone; + + const EditEntryForm({required this.entry, required this.onDone, super.key}); + + @override + State createState() => _EditEntryFormState(); +} + +class _EditEntryFormState extends State { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: TextFormField( + initialValue: widget.entry!.value.toString(), + decoration: const InputDecoration(labelText: 'Value'), + keyboardType: TextInputType.number, + validator: (value) { + try { + int.parse(value!); + } catch (e) { + return "Please enter a whole number"; + } + return null; + }, + onChanged: (newValue) { + widget.entry!.value = int.parse(newValue); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(intl.DateFormat('MM/dd/yyyy').format(widget.entry!.time)), + FilledButton( + child: const Text('Edit'), + onPressed: () async { + var result = await showDatePicker( + context: context, + initialDate: widget.entry!.time, + firstDate: DateTime.now().subtract( + const Duration(days: 365), + ), + lastDate: DateTime.now(), + ); + if (result == null) { + return; + } + setState(() { + widget.entry!.time = result; + }); + }, + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: FilledButton( + child: const Text('Cancel'), + onPressed: () { + widget.onDone(false); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: FilledButton( + child: const Text('OK'), + onPressed: () { + if (_formKey.currentState!.validate()) { + widget.onDone(true); + } + }, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart b/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart new file mode 100644 index 0000000..a8b08e4 --- /dev/null +++ b/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart @@ -0,0 +1,133 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +bool _isLargeScreen(BuildContext context) { + return MediaQuery.of(context).size.width > 960.0; +} + +bool _isMediumScreen(BuildContext context) { + return MediaQuery.of(context).size.width > 640.0; +} + +/// See bottomNavigationBarItem or NavigationRailDestination +class AdaptiveScaffoldDestination { + final String title; + final IconData icon; + + const AdaptiveScaffoldDestination({required this.title, required this.icon}); +} + +/// A widget that adapts to the current display size, displaying a [Drawer], +/// [NavigationRail], or [BottomNavigationBar]. Navigation destinations are +/// defined in the [destinations] parameter. +class AdaptiveScaffold extends StatefulWidget { + final Widget? title; + final List actions; + final Widget? body; + final int currentIndex; + final List destinations; + final ValueChanged? onNavigationIndexChange; + final FloatingActionButton? floatingActionButton; + + const AdaptiveScaffold({ + this.title, + this.body, + this.actions = const [], + required this.currentIndex, + required this.destinations, + this.onNavigationIndexChange, + this.floatingActionButton, + super.key, + }); + + @override + State createState() => _AdaptiveScaffoldState(); +} + +class _AdaptiveScaffoldState extends State { + @override + Widget build(BuildContext context) { + // Show a Drawer + if (_isLargeScreen(context)) { + return Row( + children: [ + Drawer( + child: Column( + children: [ + DrawerHeader(child: Center(child: widget.title)), + for (var d in widget.destinations) + ListTile( + leading: Icon(d.icon), + title: Text(d.title), + selected: + widget.destinations.indexOf(d) == widget.currentIndex, + onTap: () => _destinationTapped(d), + ), + ], + ), + ), + VerticalDivider(width: 1, thickness: 1, color: Colors.grey[300]), + Expanded( + child: Scaffold( + appBar: AppBar(actions: widget.actions), + body: widget.body, + floatingActionButton: widget.floatingActionButton, + ), + ), + ], + ); + } + + // Show a navigation rail + if (_isMediumScreen(context)) { + return Scaffold( + appBar: AppBar(title: widget.title, actions: widget.actions), + body: Row( + children: [ + NavigationRail( + leading: widget.floatingActionButton, + destinations: [ + ...widget.destinations.map( + (d) => NavigationRailDestination( + icon: Icon(d.icon), + label: Text(d.title), + ), + ), + ], + selectedIndex: widget.currentIndex, + onDestinationSelected: widget.onNavigationIndexChange ?? (_) {}, + ), + VerticalDivider(width: 1, thickness: 1, color: Colors.grey[300]), + Expanded(child: widget.body!), + ], + ), + ); + } + + // Show a bottom app bar + return Scaffold( + body: widget.body, + appBar: AppBar(title: widget.title, actions: widget.actions), + bottomNavigationBar: BottomNavigationBar( + items: [ + ...widget.destinations.map( + (d) => BottomNavigationBarItem(icon: Icon(d.icon), label: d.title), + ), + ], + currentIndex: widget.currentIndex, + onTap: widget.onNavigationIndexChange, + ), + floatingActionButton: widget.floatingActionButton, + ); + } + + void _destinationTapped(AdaptiveScaffoldDestination destination) { + var idx = widget.destinations.indexOf(destination); + if (idx != widget.currentIndex) { + widget.onNavigationIndexChange!(idx); + } + } +} diff --git a/web_dashboard/pubspec.yaml b/web_dashboard/pubspec.yaml new file mode 100644 index 0000000..38c4b89 --- /dev/null +++ b/web_dashboard/pubspec.yaml @@ -0,0 +1,34 @@ +name: web_dashboard +description: A dashboard app sample +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ^3.7.0-0 + +dependencies: + cloud_firestore: ^5.0.1 + community_charts_flutter: ^1.0.2 + cupertino_icons: ^1.0.0 + firebase_auth: ^5.1.0 + firebase_core: ^3.1.0 + flutter: + sdk: flutter + google_sign_in: ^6.0.0 + intl: any # Pinned by Flutter SDK version + json_annotation: ^4.5.0 + path: ^1.8.1 + provider: ^6.0.0 + uuid: ^4.0.0 + +dev_dependencies: + analysis_defaults: + path: ../../analysis_defaults + build_runner: ^2.1.0 + flutter_test: + sdk: flutter + grinder: ^0.9.0 + json_serializable: ^6.2.0 + +flutter: + uses-material-design: true diff --git a/web_dashboard/test/chart_utils_test.dart b/web_dashboard/test/chart_utils_test.dart new file mode 100644 index 0000000..02b0d8a --- /dev/null +++ b/web_dashboard/test/chart_utils_test.dart @@ -0,0 +1,30 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:web_dashboard/src/api/api.dart'; +import 'package:web_dashboard/src/utils/chart_utils.dart'; + +void main() { + group('chart utils', () { + test('totals entries by day', () async { + var entries = [ + Entry(10, DateTime(2020, 3, 1)), + Entry(10, DateTime(2020, 3, 1)), + Entry(10, DateTime(2020, 3, 2)), + ]; + var totals = entryTotalsByDay(entries, 2, today: DateTime(2020, 3, 2)); + expect(totals, hasLength(3)); + expect(totals[1].value, 20); + expect(totals[2].value, 10); + }); + test('days', () async { + expect( + DateTime.utc(2020, 1, 3).difference(DateTime.utc(2020, 1, 2)).inDays, + 1, + ); + }); + }); +} diff --git a/web_dashboard/test/mock_service_test.dart b/web_dashboard/test/mock_service_test.dart new file mode 100644 index 0000000..edca1de --- /dev/null +++ b/web_dashboard/test/mock_service_test.dart @@ -0,0 +1,108 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:web_dashboard/src/api/api.dart'; +import 'package:web_dashboard/src/api/mock.dart'; + +void main() { + group('mock dashboard API', () { + late DashboardApi api; + + setUp(() { + api = MockDashboardApi(); + }); + + group('items', () { + test('insert', () async { + var category = await api.categories.insert(Category('Coffees Drank')); + expect(category.name, 'Coffees Drank'); + }); + + test('delete', () async { + await api.categories.insert(Category('Coffees Drank')); + var category = await api.categories.insert(Category('Miles Ran')); + var removed = await api.categories.delete(category.id!); + + expect(removed, isNotNull); + expect(removed!.name, 'Miles Ran'); + + var categories = await api.categories.list(); + expect(categories, hasLength(1)); + }); + + test('update', () async { + var category = await api.categories.insert(Category('Coffees Drank')); + await api.categories.update(Category('Bagels Consumed'), category.id!); + + var latest = await api.categories.get(category.id!); + expect(latest, isNotNull); + expect(latest!.name, equals('Bagels Consumed')); + }); + test('subscribe', () async { + var stream = api.categories.subscribe(); + + stream.listen( + expectAsync1((x) { + expect(x, hasLength(1)); + expect(x.first.name, equals('Coffees Drank')); + }, count: 1), + ); + await api.categories.insert(Category('Coffees Drank')); + }); + }); + + group('entry service', () { + late Category category; + DateTime dateTime = DateTime(2020, 1, 1, 30, 45); + + setUp(() async { + category = await api.categories.insert( + Category('Lines of code committed'), + ); + }); + + test('insert', () async { + var entry = await api.entries.insert(category.id!, Entry(1, dateTime)); + + expect(entry.value, 1); + expect(entry.time, dateTime); + }); + + test('delete', () async { + await api.entries.insert(category.id!, Entry(1, dateTime)); + var entry2 = await api.entries.insert(category.id!, Entry(2, dateTime)); + + await api.entries.delete(category.id!, entry2.id!); + + var entries = await api.entries.list(category.id!); + expect(entries, hasLength(1)); + }); + + test('update', () async { + var entry = await api.entries.insert(category.id!, Entry(1, dateTime)); + var updated = await api.entries.update( + category.id!, + entry.id!, + Entry(2, dateTime), + ); + expect(updated.value, 2); + }); + + test('subscribe', () async { + var stream = api.entries.subscribe(category.id!); + + stream.listen( + expectAsync1((x) { + expect(x, hasLength(1)); + expect(x.first.value, equals(1)); + }, count: 1), + ); + + await api.entries.insert(category.id!, Entry(1, dateTime)); + }); + }); + }); +} diff --git a/web_dashboard/tool/grind.dart b/web_dashboard/tool/grind.dart new file mode 100644 index 0000000..1728347 --- /dev/null +++ b/web_dashboard/tool/grind.dart @@ -0,0 +1,117 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:convert'; +import 'dart:io'; + +import 'package:grinder/grinder.dart'; +import 'package:path/path.dart' as path; + +void main(List args) => grind(args); + +@Task() +void runSkia() { + run( + 'flutter', + arguments: + 'run -d web --web-port=5000 --release --dart-define=FLUTTER_WEB_USE_SKIA=true lib/main.dart ' + .split(' '), + ); +} + +@Task() +void runWeb() { + run( + 'flutter', + arguments: 'run -d web --web-port=5000 lib/main.dart '.split(' '), + ); +} + +@Task() +void runFirebase() { + run( + 'flutter', + arguments: 'run -d web --web-port=5000 lib/main_firebase.dart '.split(' '), + ); +} + +@Task() +void runFirebaseSkia() { + run( + 'flutter', + arguments: + 'run -d web --web-port=5000 --release --dart-define=FLUTTER_WEB_USE_SKIA=true lib/main_firebase.dart' + .split(' '), + ); +} + +@Task() +void test() { + TestRunner().testAsync(); +} + +@DefaultTask() +@Depends(test, copyright) +void build() { + Pub.build(); +} + +@Task() +void clean() => defaultClean(); + +@Task() +void generate() { + Pub.run('build_runner', arguments: ['build']); +} + +const _copyright = + '''// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file.'''; + +@Task() +Future copyright() async { + var files = []; + await for (var file in _filesWithoutCopyright()) { + files.add(file); + } + + if (files.isNotEmpty) { + log('Found Dart files without a copyright header:'); + for (var file in files) { + log(file.toString()); + } + fail('run "grind fix-copyright" to add copyright headers'); + } +} + +@Task() +Future fixCopyright() async { + await for (var file in _filesWithoutCopyright()) { + var contents = await file.readAsString(); + await file.writeAsString('$_copyright\n\n$contents'); + } +} + +Stream _filesWithoutCopyright() async* { + var set = FileSet.fromDir(Directory('.'), recurse: true); + var dartFiles = set.files.where( + (file) => path.extension(file.path) == '.dart', + ); + + for (var file in dartFiles) { + var firstThreeLines = await file + .openRead() + .transform(utf8.decoder) + .transform(const LineSplitter()) + .take(3) + .fold('', (previous, element) { + if (previous == '') return element; + return '$previous\n$element'; + }); + + if (firstThreeLines != _copyright) { + yield file; + } + } +} diff --git a/web_dashboard/web/firebase_init.js b/web_dashboard/web/firebase_init.js new file mode 100644 index 0000000..54f5523 --- /dev/null +++ b/web_dashboard/web/firebase_init.js @@ -0,0 +1,12 @@ +// Your web app's Firebase configuration +var firebaseConfig = { + apiKey: "", + authDomain: "", + databaseURL: "", + projectId: "", + storageBucket: "", + messagingSenderId: "", + appId: "" +}; +// Initialize Firebase +firebase.initializeApp(firebaseConfig); diff --git a/web_dashboard/web/icons/Icon-192.png b/web_dashboard/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web_dashboard/web/icons/Icon-192.png differ diff --git a/web_dashboard/web/icons/Icon-512.png b/web_dashboard/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web_dashboard/web/icons/Icon-512.png differ diff --git a/web_dashboard/web/index.html b/web_dashboard/web/index.html new file mode 100644 index 0000000..1717113 --- /dev/null +++ b/web_dashboard/web/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + web_dashboard + + + + + + + + + + + + + + + + + diff --git a/web_dashboard/web/manifest.json b/web_dashboard/web/manifest.json new file mode 100644 index 0000000..0aeff59 --- /dev/null +++ b/web_dashboard/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "web_dashboard", + "short_name": "web_dashboard", + "start_url": ".", + "display": "minimal-ui", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A desktop-friendly dashboard app", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +}